@nathapp/nax 0.32.2 → 0.34.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/README.md +191 -6
- package/dist/nax.js +1150 -382
- 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/crash-recovery.ts +8 -0
- package/src/execution/escalation/tier-escalation.ts +9 -2
- package/src/execution/iteration-runner.ts +2 -0
- package/src/execution/lifecycle/run-completion.ts +100 -15
- package/src/execution/parallel-executor.ts +20 -1
- package/src/execution/pipeline-result-handler.ts +5 -1
- package/src/execution/runner.ts +20 -0
- package/src/execution/sequential-executor.ts +2 -11
- package/src/hooks/types.ts +20 -10
- package/src/interaction/index.ts +1 -0
- package/src/interaction/triggers.ts +21 -0
- package/src/interaction/types.ts +7 -0
- package/src/metrics/tracker.ts +7 -0
- package/src/metrics/types.ts +2 -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 +7 -2
- package/src/prd/types.ts +17 -2
- package/src/review/orchestrator.ts +1 -0
- package/src/review/types.ts +2 -0
- package/src/tdd/isolation.ts +1 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decompose module types.
|
|
3
|
+
*
|
|
4
|
+
* DecomposeConfig, SubStory, DecomposeResult, ValidationResult.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Configuration for story decomposition */
|
|
8
|
+
export interface DecomposeConfig {
|
|
9
|
+
/** Maximum number of sub-stories to generate */
|
|
10
|
+
maxSubStories: number;
|
|
11
|
+
/** Maximum allowed complexity for any sub-story */
|
|
12
|
+
maxComplexity: "simple" | "medium" | "complex" | "expert";
|
|
13
|
+
/** Maximum number of retries on validation failure */
|
|
14
|
+
maxRetries?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A single decomposed sub-story */
|
|
18
|
+
export interface SubStory {
|
|
19
|
+
/** Sub-story ID (e.g., "SD-001-1") */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Parent story ID */
|
|
22
|
+
parentStoryId: string;
|
|
23
|
+
/** Sub-story title */
|
|
24
|
+
title: string;
|
|
25
|
+
/** Sub-story description */
|
|
26
|
+
description: string;
|
|
27
|
+
/** Acceptance criteria */
|
|
28
|
+
acceptanceCriteria: string[];
|
|
29
|
+
/** Tags for routing */
|
|
30
|
+
tags: string[];
|
|
31
|
+
/** Dependencies (story IDs) */
|
|
32
|
+
dependencies: string[];
|
|
33
|
+
/** Complexity classification */
|
|
34
|
+
complexity: "simple" | "medium" | "complex" | "expert";
|
|
35
|
+
/** Justification that this sub-story does not overlap with sibling stories */
|
|
36
|
+
nonOverlapJustification: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Validation result for decomposition output */
|
|
40
|
+
export interface ValidationResult {
|
|
41
|
+
valid: boolean;
|
|
42
|
+
errors: string[];
|
|
43
|
+
warnings: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Result of decomposing a story */
|
|
47
|
+
export interface DecomposeResult {
|
|
48
|
+
subStories: SubStory[];
|
|
49
|
+
validation: ValidationResult;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Adapter interface for calling the decompose LLM */
|
|
53
|
+
export interface DecomposeAdapter {
|
|
54
|
+
decompose(prompt: string): Promise<string>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complexity validator.
|
|
3
|
+
*
|
|
4
|
+
* Validates each substory complexity is <= config.maxSubstoryComplexity (default: medium).
|
|
5
|
+
* Reuses classifyComplexity() from src/routing/router.ts as a cross-check against LLM-assigned complexity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { classifyComplexity } from "../../routing";
|
|
9
|
+
import type { SubStory, ValidationResult } from "../types";
|
|
10
|
+
|
|
11
|
+
export type ComplexityLevel = "simple" | "medium" | "complex" | "expert";
|
|
12
|
+
|
|
13
|
+
const COMPLEXITY_ORDER: Record<ComplexityLevel, number> = {
|
|
14
|
+
simple: 0,
|
|
15
|
+
medium: 1,
|
|
16
|
+
complex: 2,
|
|
17
|
+
expert: 3,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function validateComplexity(substories: SubStory[], maxComplexity: ComplexityLevel): ValidationResult {
|
|
21
|
+
const errors: string[] = [];
|
|
22
|
+
const warnings: string[] = [];
|
|
23
|
+
const maxOrder = COMPLEXITY_ORDER[maxComplexity];
|
|
24
|
+
|
|
25
|
+
for (const sub of substories) {
|
|
26
|
+
const assignedOrder = COMPLEXITY_ORDER[sub.complexity];
|
|
27
|
+
|
|
28
|
+
if (assignedOrder > maxOrder) {
|
|
29
|
+
errors.push(`Substory ${sub.id} complexity "${sub.complexity}" exceeds maxComplexity "${maxComplexity}"`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Cross-check with classifyComplexity
|
|
33
|
+
const classified = classifyComplexity(sub.title, sub.description, sub.acceptanceCriteria, sub.tags);
|
|
34
|
+
if (classified !== sub.complexity) {
|
|
35
|
+
const classifiedOrder = COMPLEXITY_ORDER[classified as ComplexityLevel] ?? 0;
|
|
36
|
+
if (classifiedOrder > assignedOrder) {
|
|
37
|
+
warnings.push(
|
|
38
|
+
`Substory ${sub.id} is assigned complexity "${sub.complexity}" but classifier estimates "${classified}" — may be underestimated`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
45
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage validator.
|
|
3
|
+
*
|
|
4
|
+
* Checks that the union of substory acceptance criteria covers
|
|
5
|
+
* the original story's AC using keyword matching.
|
|
6
|
+
* Warns on unmatched original criteria.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { UserStory } from "../../prd";
|
|
10
|
+
import type { SubStory, ValidationResult } from "../types";
|
|
11
|
+
|
|
12
|
+
const STOP_WORDS = new Set([
|
|
13
|
+
"a",
|
|
14
|
+
"an",
|
|
15
|
+
"the",
|
|
16
|
+
"and",
|
|
17
|
+
"or",
|
|
18
|
+
"but",
|
|
19
|
+
"is",
|
|
20
|
+
"are",
|
|
21
|
+
"was",
|
|
22
|
+
"were",
|
|
23
|
+
"be",
|
|
24
|
+
"been",
|
|
25
|
+
"being",
|
|
26
|
+
"have",
|
|
27
|
+
"has",
|
|
28
|
+
"had",
|
|
29
|
+
"do",
|
|
30
|
+
"does",
|
|
31
|
+
"did",
|
|
32
|
+
"will",
|
|
33
|
+
"would",
|
|
34
|
+
"could",
|
|
35
|
+
"should",
|
|
36
|
+
"may",
|
|
37
|
+
"might",
|
|
38
|
+
"can",
|
|
39
|
+
"to",
|
|
40
|
+
"of",
|
|
41
|
+
"in",
|
|
42
|
+
"on",
|
|
43
|
+
"at",
|
|
44
|
+
"for",
|
|
45
|
+
"with",
|
|
46
|
+
"by",
|
|
47
|
+
"from",
|
|
48
|
+
"as",
|
|
49
|
+
"it",
|
|
50
|
+
"its",
|
|
51
|
+
"that",
|
|
52
|
+
"this",
|
|
53
|
+
"these",
|
|
54
|
+
"those",
|
|
55
|
+
"not",
|
|
56
|
+
"no",
|
|
57
|
+
"so",
|
|
58
|
+
"if",
|
|
59
|
+
"then",
|
|
60
|
+
"than",
|
|
61
|
+
"when",
|
|
62
|
+
"which",
|
|
63
|
+
"who",
|
|
64
|
+
"what",
|
|
65
|
+
"how",
|
|
66
|
+
"all",
|
|
67
|
+
"each",
|
|
68
|
+
"any",
|
|
69
|
+
"up",
|
|
70
|
+
"out",
|
|
71
|
+
"about",
|
|
72
|
+
"into",
|
|
73
|
+
"through",
|
|
74
|
+
"after",
|
|
75
|
+
"before",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
function extractKeywords(text: string): string[] {
|
|
79
|
+
return text
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.split(/[\s,.:;!?()\[\]{}"'`\-_/\\]+/)
|
|
82
|
+
.filter((w) => w.length > 2 && !STOP_WORDS.has(w));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function commonPrefixLength(a: string, b: string): number {
|
|
86
|
+
let i = 0;
|
|
87
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
88
|
+
return i;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Two keywords match if they are identical or share a common prefix of ≥5 chars.
|
|
93
|
+
* This handles morphological variants like "register" / "registration" (prefix "regist" = 6).
|
|
94
|
+
* It does NOT match unrelated words that start with "re" ("reset" vs "register" = 2 chars).
|
|
95
|
+
*/
|
|
96
|
+
function keywordsMatch(a: string, b: string): boolean {
|
|
97
|
+
return a === b || commonPrefixLength(a, b) >= 5;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns true if the original AC is "covered" by the union of substory ACs.
|
|
102
|
+
* Covered means: strictly more than half of the original AC's keywords have a
|
|
103
|
+
* match (exact or common-prefix ≥5) in the substory AC keywords.
|
|
104
|
+
*/
|
|
105
|
+
function isCovered(originalAc: string, substoryAcs: string[]): boolean {
|
|
106
|
+
const originalKw = extractKeywords(originalAc);
|
|
107
|
+
if (originalKw.length === 0) return true;
|
|
108
|
+
|
|
109
|
+
const substoryKwList = substoryAcs.flatMap(extractKeywords);
|
|
110
|
+
|
|
111
|
+
let matchCount = 0;
|
|
112
|
+
for (const kw of originalKw) {
|
|
113
|
+
if (substoryKwList.some((s) => keywordsMatch(kw, s))) {
|
|
114
|
+
matchCount++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Require strictly more than half of the original AC's keywords to match
|
|
119
|
+
return matchCount > originalKw.length / 2;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function validateCoverage(originalStory: UserStory, substories: SubStory[]): ValidationResult {
|
|
123
|
+
const warnings: string[] = [];
|
|
124
|
+
|
|
125
|
+
const allSubstoryAcs = substories.flatMap((s) => s.acceptanceCriteria);
|
|
126
|
+
|
|
127
|
+
for (const ac of originalStory.acceptanceCriteria ?? []) {
|
|
128
|
+
if (!isCovered(ac, allSubstoryAcs)) {
|
|
129
|
+
warnings.push(`Original AC not covered by any substory: "${ac}"`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { valid: true, errors: [], warnings };
|
|
134
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency validator.
|
|
3
|
+
*
|
|
4
|
+
* Validates:
|
|
5
|
+
* - No circular dependencies among substories
|
|
6
|
+
* - All referenced dependency IDs exist (in substories or existing PRD)
|
|
7
|
+
* - No ID collisions with existing PRD story IDs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SubStory, ValidationResult } from "../types";
|
|
11
|
+
|
|
12
|
+
function detectCycles(substories: SubStory[]): string[] {
|
|
13
|
+
const errors: string[] = [];
|
|
14
|
+
const idSet = new Set(substories.map((s) => s.id));
|
|
15
|
+
|
|
16
|
+
// Build adjacency map (only edges within the substory set)
|
|
17
|
+
const adj: Map<string, string[]> = new Map();
|
|
18
|
+
for (const sub of substories) {
|
|
19
|
+
adj.set(
|
|
20
|
+
sub.id,
|
|
21
|
+
sub.dependencies.filter((d) => idSet.has(d)),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const WHITE = 0; // unvisited
|
|
26
|
+
const GRAY = 1; // in current path
|
|
27
|
+
const BLACK = 2; // done
|
|
28
|
+
|
|
29
|
+
const color: Map<string, number> = new Map();
|
|
30
|
+
for (const id of idSet) color.set(id, WHITE);
|
|
31
|
+
|
|
32
|
+
const reported = new Set<string>();
|
|
33
|
+
|
|
34
|
+
function dfs(id: string, path: string[]): void {
|
|
35
|
+
color.set(id, GRAY);
|
|
36
|
+
for (const dep of adj.get(id) ?? []) {
|
|
37
|
+
if (color.get(dep) === GRAY) {
|
|
38
|
+
// Found a cycle — report it once
|
|
39
|
+
const cycleKey = [...path, dep].sort().join(",");
|
|
40
|
+
if (!reported.has(cycleKey)) {
|
|
41
|
+
reported.add(cycleKey);
|
|
42
|
+
const cycleStart = path.indexOf(dep);
|
|
43
|
+
const cycleNodes = cycleStart >= 0 ? path.slice(cycleStart) : path;
|
|
44
|
+
const cycleStr = [...cycleNodes, dep].join(" -> ");
|
|
45
|
+
errors.push(`Circular dependency detected: ${cycleStr}`);
|
|
46
|
+
}
|
|
47
|
+
} else if (color.get(dep) === WHITE) {
|
|
48
|
+
dfs(dep, [...path, dep]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
color.set(id, BLACK);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const id of idSet) {
|
|
55
|
+
if (color.get(id) === WHITE) {
|
|
56
|
+
dfs(id, [id]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return errors;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function validateDependencies(substories: SubStory[], existingStoryIds: string[]): ValidationResult {
|
|
64
|
+
const errors: string[] = [];
|
|
65
|
+
|
|
66
|
+
const substoryIdSet = new Set(substories.map((s) => s.id));
|
|
67
|
+
const existingIdSet = new Set(existingStoryIds);
|
|
68
|
+
const allKnownIds = new Set([...substoryIdSet, ...existingIdSet]);
|
|
69
|
+
|
|
70
|
+
// ID collisions with existing PRD
|
|
71
|
+
for (const sub of substories) {
|
|
72
|
+
if (existingIdSet.has(sub.id)) {
|
|
73
|
+
errors.push(`Substory ID "${sub.id}" collides with existing PRD story — duplicate IDs are not allowed`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Non-existent dependency references
|
|
78
|
+
for (const sub of substories) {
|
|
79
|
+
for (const dep of sub.dependencies) {
|
|
80
|
+
if (!allKnownIds.has(dep)) {
|
|
81
|
+
errors.push(`Substory ${sub.id} references non-existent story ID "${dep}"`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Circular dependencies
|
|
87
|
+
const cycleErrors = detectCycles(substories);
|
|
88
|
+
errors.push(...cycleErrors);
|
|
89
|
+
|
|
90
|
+
return { valid: errors.length === 0, errors, warnings: [] };
|
|
91
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validator orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* runAllValidators() runs all validators in sequence and returns merged ValidationResult.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { UserStory } from "../../prd";
|
|
8
|
+
import type { DecomposeConfig, SubStory, ValidationResult } from "../types";
|
|
9
|
+
import type { ComplexityLevel } from "./complexity";
|
|
10
|
+
import { validateComplexity } from "./complexity";
|
|
11
|
+
import { validateCoverage } from "./coverage";
|
|
12
|
+
import { validateDependencies } from "./dependency";
|
|
13
|
+
import { validateOverlap } from "./overlap";
|
|
14
|
+
|
|
15
|
+
export function runAllValidators(
|
|
16
|
+
originalStory: UserStory,
|
|
17
|
+
substories: SubStory[],
|
|
18
|
+
existingStories: UserStory[],
|
|
19
|
+
config: DecomposeConfig,
|
|
20
|
+
): ValidationResult {
|
|
21
|
+
const existingIds = existingStories.map((s) => s.id);
|
|
22
|
+
const maxComplexity = (config.maxComplexity ?? "medium") as ComplexityLevel;
|
|
23
|
+
|
|
24
|
+
const results = [
|
|
25
|
+
validateOverlap(substories, existingStories),
|
|
26
|
+
validateCoverage(originalStory, substories),
|
|
27
|
+
validateComplexity(substories, maxComplexity),
|
|
28
|
+
validateDependencies(substories, existingIds),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const errors = results.flatMap((r) => r.errors);
|
|
32
|
+
const warnings = results.flatMap((r) => r.warnings);
|
|
33
|
+
|
|
34
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
35
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlap validator.
|
|
3
|
+
*
|
|
4
|
+
* Checks keyword + tag similarity between each substory and all existing PRD stories.
|
|
5
|
+
* Uses Jaccard-like normalized keyword intersection over title + tags.
|
|
6
|
+
* Flags pairs with similarity > 0.6 as warnings, > 0.8 as errors.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { UserStory } from "../../prd";
|
|
10
|
+
import type { SubStory, ValidationResult } from "../types";
|
|
11
|
+
|
|
12
|
+
const STOP_WORDS = new Set([
|
|
13
|
+
"a",
|
|
14
|
+
"an",
|
|
15
|
+
"the",
|
|
16
|
+
"and",
|
|
17
|
+
"or",
|
|
18
|
+
"but",
|
|
19
|
+
"is",
|
|
20
|
+
"are",
|
|
21
|
+
"was",
|
|
22
|
+
"were",
|
|
23
|
+
"be",
|
|
24
|
+
"been",
|
|
25
|
+
"being",
|
|
26
|
+
"have",
|
|
27
|
+
"has",
|
|
28
|
+
"had",
|
|
29
|
+
"do",
|
|
30
|
+
"does",
|
|
31
|
+
"did",
|
|
32
|
+
"will",
|
|
33
|
+
"would",
|
|
34
|
+
"could",
|
|
35
|
+
"should",
|
|
36
|
+
"may",
|
|
37
|
+
"might",
|
|
38
|
+
"can",
|
|
39
|
+
"to",
|
|
40
|
+
"of",
|
|
41
|
+
"in",
|
|
42
|
+
"on",
|
|
43
|
+
"at",
|
|
44
|
+
"for",
|
|
45
|
+
"with",
|
|
46
|
+
"by",
|
|
47
|
+
"from",
|
|
48
|
+
"as",
|
|
49
|
+
"it",
|
|
50
|
+
"its",
|
|
51
|
+
"that",
|
|
52
|
+
"this",
|
|
53
|
+
"these",
|
|
54
|
+
"those",
|
|
55
|
+
"not",
|
|
56
|
+
"no",
|
|
57
|
+
"so",
|
|
58
|
+
"if",
|
|
59
|
+
"then",
|
|
60
|
+
"than",
|
|
61
|
+
"when",
|
|
62
|
+
"which",
|
|
63
|
+
"who",
|
|
64
|
+
"what",
|
|
65
|
+
"how",
|
|
66
|
+
"all",
|
|
67
|
+
"each",
|
|
68
|
+
"any",
|
|
69
|
+
"up",
|
|
70
|
+
"out",
|
|
71
|
+
"about",
|
|
72
|
+
"into",
|
|
73
|
+
"through",
|
|
74
|
+
"after",
|
|
75
|
+
"before",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
function extractKeywords(texts: string[]): Set<string> {
|
|
79
|
+
const words = texts
|
|
80
|
+
.join(" ")
|
|
81
|
+
.toLowerCase()
|
|
82
|
+
.split(/[\s,.:;!?()\[\]{}"'`\-_/\\]+/)
|
|
83
|
+
.filter((w) => w.length > 2 && !STOP_WORDS.has(w) && !/^\d+$/.test(w));
|
|
84
|
+
return new Set(words);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function jaccardSimilarity(a: Set<string>, b: Set<string>): number {
|
|
88
|
+
if (a.size === 0 && b.size === 0) return 0;
|
|
89
|
+
let intersectionSize = 0;
|
|
90
|
+
for (const word of a) {
|
|
91
|
+
if (b.has(word)) intersectionSize++;
|
|
92
|
+
}
|
|
93
|
+
const unionSize = a.size + b.size - intersectionSize;
|
|
94
|
+
return unionSize === 0 ? 0 : intersectionSize / unionSize;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Use title + tags only for overlap detection to get stable, meaningful similarity scores
|
|
98
|
+
function substoryKeywords(s: SubStory): Set<string> {
|
|
99
|
+
return extractKeywords([s.title, ...s.tags]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function storyKeywords(s: UserStory): Set<string> {
|
|
103
|
+
return extractKeywords([s.title, ...(s.tags ?? [])]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function validateOverlap(substories: SubStory[], existingStories: UserStory[]): ValidationResult {
|
|
107
|
+
const errors: string[] = [];
|
|
108
|
+
const warnings: string[] = [];
|
|
109
|
+
|
|
110
|
+
for (const sub of substories) {
|
|
111
|
+
const subKw = substoryKeywords(sub);
|
|
112
|
+
for (const existing of existingStories) {
|
|
113
|
+
const exKw = storyKeywords(existing);
|
|
114
|
+
const sim = jaccardSimilarity(subKw, exKw);
|
|
115
|
+
if (sim > 0.8) {
|
|
116
|
+
errors.push(
|
|
117
|
+
`Substory ${sub.id} overlaps with existing story ${existing.id} (similarity ${sim.toFixed(2)} > 0.8)`,
|
|
118
|
+
);
|
|
119
|
+
} else if (sim > 0.6) {
|
|
120
|
+
warnings.push(
|
|
121
|
+
`Substory ${sub.id} may overlap with existing story ${existing.id} (similarity ${sim.toFixed(2)} > 0.6)`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
128
|
+
}
|
|
@@ -409,3 +409,11 @@ export function resetCrashHandlers(): void {
|
|
|
409
409
|
handlersInstalled = false;
|
|
410
410
|
stopHeartbeat();
|
|
411
411
|
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Returns true if heartbeat timer is currently active.
|
|
415
|
+
* @internal - test use only.
|
|
416
|
+
*/
|
|
417
|
+
export function _isHeartbeatActive(): boolean {
|
|
418
|
+
return heartbeatTimer !== null;
|
|
419
|
+
}
|
|
@@ -20,12 +20,17 @@ import { appendProgress } from "../progress";
|
|
|
20
20
|
import { handleMaxAttemptsReached, handleNoTierAvailable } from "./tier-outcome";
|
|
21
21
|
|
|
22
22
|
/** Build a StructuredFailure for tier escalation. */
|
|
23
|
-
function buildEscalationFailure(
|
|
23
|
+
function buildEscalationFailure(
|
|
24
|
+
story: UserStory,
|
|
25
|
+
currentTier: string,
|
|
26
|
+
reviewFindings?: import("../../plugins/types").ReviewFinding[],
|
|
27
|
+
): StructuredFailure {
|
|
24
28
|
return {
|
|
25
29
|
attempt: (story.attempts ?? 0) + 1,
|
|
26
30
|
modelTier: currentTier,
|
|
27
31
|
stage: "escalation" as const,
|
|
28
32
|
summary: `Failed with tier ${currentTier}, escalating to next tier`,
|
|
33
|
+
reviewFindings: reviewFindings && reviewFindings.length > 0 ? reviewFindings : undefined,
|
|
29
34
|
timestamp: new Date().toISOString(),
|
|
30
35
|
};
|
|
31
36
|
}
|
|
@@ -192,6 +197,7 @@ export interface EscalationHandlerContext {
|
|
|
192
197
|
context: {
|
|
193
198
|
retryAsLite?: boolean;
|
|
194
199
|
tddFailureCategory?: FailureCategory;
|
|
200
|
+
reviewFindings?: import("../../plugins/types").ReviewFinding[];
|
|
195
201
|
};
|
|
196
202
|
};
|
|
197
203
|
config: NaxConfig;
|
|
@@ -224,6 +230,7 @@ export async function handleTierEscalation(ctx: EscalationHandlerContext): Promi
|
|
|
224
230
|
// Retrieve TDD-specific context flags set by executionStage
|
|
225
231
|
const escalateRetryAsLite = ctx.pipelineResult.context.retryAsLite === true;
|
|
226
232
|
const escalateFailureCategory = ctx.pipelineResult.context.tddFailureCategory;
|
|
233
|
+
const escalateReviewFindings = ctx.pipelineResult.context.reviewFindings;
|
|
227
234
|
// S5: Auto-switch to test-after on greenfield-no-tests
|
|
228
235
|
const escalateRetryAsTestAfter = escalateFailureCategory === "greenfield-no-tests";
|
|
229
236
|
const routingMode = ctx.config.routing.llm?.mode ?? "hybrid";
|
|
@@ -288,7 +295,7 @@ export async function handleTierEscalation(ctx: EscalationHandlerContext): Promi
|
|
|
288
295
|
const shouldResetAttempts = isChangingTier || shouldSwitchToTestAfter;
|
|
289
296
|
|
|
290
297
|
// Build escalation failure
|
|
291
|
-
const escalationFailure = buildEscalationFailure(s, currentStoryTier);
|
|
298
|
+
const escalationFailure = buildEscalationFailure(s, currentStoryTier, escalateReviewFindings);
|
|
292
299
|
|
|
293
300
|
return {
|
|
294
301
|
...s,
|
|
@@ -58,6 +58,7 @@ export async function runIteration(
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
const storyStartTime = Date.now();
|
|
61
62
|
const storyGitRef = await captureGitRef(ctx.workdir);
|
|
62
63
|
const pipelineContext: PipelineContext = {
|
|
63
64
|
config: ctx.config,
|
|
@@ -109,6 +110,7 @@ export async function runIteration(
|
|
|
109
110
|
allStoryMetrics,
|
|
110
111
|
storyGitRef,
|
|
111
112
|
interactionChain: ctx.interactionChain,
|
|
113
|
+
storyStartTime,
|
|
112
114
|
};
|
|
113
115
|
|
|
114
116
|
if (pipelineResult.success) {
|