@quinteroac/agents-coding-toolkit 0.1.0-preview

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 (85) hide show
  1. package/AGENTS.md +7 -0
  2. package/README.md +127 -0
  3. package/package.json +34 -0
  4. package/scaffold/.agents/flow/archived/tmpl_.gitkeep +0 -0
  5. package/scaffold/.agents/flow/tmpl_README.md +7 -0
  6. package/scaffold/.agents/flow/tmpl_iteration_close_checklist.example.md +11 -0
  7. package/scaffold/.agents/skills/automated-fix/tmpl_SKILL.md +67 -0
  8. package/scaffold/.agents/skills/create-issue/tmpl_SKILL.md +68 -0
  9. package/scaffold/.agents/skills/create-pr-document/tmpl_SKILL.md +125 -0
  10. package/scaffold/.agents/skills/create-project-context/tmpl_SKILL.md +168 -0
  11. package/scaffold/.agents/skills/create-test-plan/tmpl_SKILL.md +86 -0
  12. package/scaffold/.agents/skills/debug/tmpl_SKILL.md +19 -0
  13. package/scaffold/.agents/skills/evaluate/tmpl_SKILL.md +19 -0
  14. package/scaffold/.agents/skills/execute-test-batch/tmpl_SKILL.md +49 -0
  15. package/scaffold/.agents/skills/execute-test-case/tmpl_SKILL.md +47 -0
  16. package/scaffold/.agents/skills/implement-user-story/tmpl_SKILL.md +68 -0
  17. package/scaffold/.agents/skills/plan-refactor/tmpl_SKILL.md +19 -0
  18. package/scaffold/.agents/skills/refactor-prd/tmpl_SKILL.md +19 -0
  19. package/scaffold/.agents/skills/refine-pr-document/tmpl_SKILL.md +108 -0
  20. package/scaffold/.agents/skills/refine-project-context/tmpl_SKILL.md +157 -0
  21. package/scaffold/.agents/skills/refine-test-plan/tmpl_SKILL.md +76 -0
  22. package/scaffold/.agents/tmpl_PROJECT_CONTEXT.md +3 -0
  23. package/scaffold/.agents/tmpl_state.example.json +26 -0
  24. package/scaffold/.agents/tmpl_state_rules.md +29 -0
  25. package/scaffold/docs/nvst-flow/templates/tmpl_CHANGELOG.md +18 -0
  26. package/scaffold/docs/nvst-flow/templates/tmpl_TECHNICAL_DEBT.md +11 -0
  27. package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_evaluation-report.md +19 -0
  28. package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_product-requirement-document.md +19 -0
  29. package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_refactor_plan.md +19 -0
  30. package/scaffold/docs/nvst-flow/templates/tmpl_it_000001_test-plan.md +19 -0
  31. package/scaffold/docs/nvst-flow/tmpl_COMMANDS.md +0 -0
  32. package/scaffold/docs/nvst-flow/tmpl_QUICK_USE.md +0 -0
  33. package/scaffold/docs/tmpl_PLACEHOLDER.md +0 -0
  34. package/scaffold/schemas/node-shims.d.ts +15 -0
  35. package/scaffold/schemas/tmpl_issues.ts +19 -0
  36. package/scaffold/schemas/tmpl_prd.ts +26 -0
  37. package/scaffold/schemas/tmpl_progress.ts +39 -0
  38. package/scaffold/schemas/tmpl_state.ts +81 -0
  39. package/scaffold/schemas/tmpl_test-plan.ts +20 -0
  40. package/scaffold/schemas/tmpl_validate-progress.ts +13 -0
  41. package/scaffold/schemas/tmpl_validate-state.ts +13 -0
  42. package/scaffold/tmpl_AGENTS.md +7 -0
  43. package/schemas/prd.ts +26 -0
  44. package/schemas/progress.ts +39 -0
  45. package/schemas/state.ts +81 -0
  46. package/schemas/test-plan.test.ts +53 -0
  47. package/schemas/test-plan.ts +20 -0
  48. package/schemas/validate-progress.ts +13 -0
  49. package/schemas/validate-state.ts +13 -0
  50. package/src/agent.test.ts +37 -0
  51. package/src/agent.ts +225 -0
  52. package/src/cli-path.ts +4 -0
  53. package/src/cli.ts +578 -0
  54. package/src/commands/approve-project-context.ts +37 -0
  55. package/src/commands/approve-requirement.ts +217 -0
  56. package/src/commands/approve-test-plan.test.ts +193 -0
  57. package/src/commands/approve-test-plan.ts +202 -0
  58. package/src/commands/create-issue.test.ts +484 -0
  59. package/src/commands/create-issue.ts +371 -0
  60. package/src/commands/create-project-context.ts +96 -0
  61. package/src/commands/create-prototype.test.ts +153 -0
  62. package/src/commands/create-prototype.ts +425 -0
  63. package/src/commands/create-test-plan.test.ts +381 -0
  64. package/src/commands/create-test-plan.ts +248 -0
  65. package/src/commands/define-requirement.ts +47 -0
  66. package/src/commands/destroy.ts +113 -0
  67. package/src/commands/execute-automated-fix.test.ts +580 -0
  68. package/src/commands/execute-automated-fix.ts +363 -0
  69. package/src/commands/execute-manual-fix.test.ts +343 -0
  70. package/src/commands/execute-manual-fix.ts +203 -0
  71. package/src/commands/execute-test-plan.test.ts +1891 -0
  72. package/src/commands/execute-test-plan.ts +722 -0
  73. package/src/commands/init.ts +85 -0
  74. package/src/commands/refine-project-context.ts +74 -0
  75. package/src/commands/refine-requirement.ts +60 -0
  76. package/src/commands/refine-test-plan.test.ts +200 -0
  77. package/src/commands/refine-test-plan.ts +93 -0
  78. package/src/commands/start-iteration.test.ts +144 -0
  79. package/src/commands/start-iteration.ts +101 -0
  80. package/src/commands/write-json.ts +136 -0
  81. package/src/install.test.ts +124 -0
  82. package/src/pack.test.ts +103 -0
  83. package/src/state.test.ts +66 -0
  84. package/src/state.ts +52 -0
  85. package/tsconfig.json +15 -0
@@ -0,0 +1,217 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { $ } from "bun";
4
+
5
+ import { CLI_PATH } from "../cli-path";
6
+ import type { Prd } from "../../scaffold/schemas/tmpl_prd";
7
+ import { exists, readState, writeState, FLOW_REL_DIR } from "../state";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Markdown PRD parser
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * Parses the Markdown PRD into a structured object matching PrdSchema.
15
+ *
16
+ * Expected sections:
17
+ * ## Goals — bullet list of goal strings
18
+ * ### US-xxx: Title — user story blocks with **Acceptance Criteria:** sub-list
19
+ * ## Functional Requirements — bullet list of "FR-x: description"
20
+ */
21
+ function parsePrd(markdown: string): Prd {
22
+ const goals: string[] = [];
23
+ const userStories: Prd["userStories"] = [];
24
+ const functionalRequirements: Prd["functionalRequirements"] = [];
25
+
26
+ const lines = markdown.split("\n");
27
+
28
+ let currentSection: "goals" | "user-stories" | "functional-requirements" | null = null;
29
+ let currentStory: Prd["userStories"][number] | null = null;
30
+ let inAcceptanceCriteria = false;
31
+
32
+ for (const line of lines) {
33
+ const trimmed = line.trim();
34
+
35
+ // Detect section headers
36
+ if (/^## Goals/i.test(trimmed)) {
37
+ currentSection = "goals";
38
+ currentStory = null;
39
+ inAcceptanceCriteria = false;
40
+ continue;
41
+ }
42
+ if (/^## User Stories/i.test(trimmed)) {
43
+ currentSection = "user-stories";
44
+ currentStory = null;
45
+ inAcceptanceCriteria = false;
46
+ continue;
47
+ }
48
+ if (/^## Functional Requirements/i.test(trimmed)) {
49
+ // Flush any pending user story
50
+ if (currentStory) {
51
+ userStories.push(currentStory);
52
+ currentStory = null;
53
+ }
54
+ currentSection = "functional-requirements";
55
+ inAcceptanceCriteria = false;
56
+ continue;
57
+ }
58
+ // Skip other level-2 sections (e.g. ## Context, ## Non-Goals)
59
+ if (/^## /.test(trimmed)) {
60
+ if (currentStory) {
61
+ userStories.push(currentStory);
62
+ currentStory = null;
63
+ }
64
+ currentSection = null;
65
+ inAcceptanceCriteria = false;
66
+ continue;
67
+ }
68
+
69
+ // --- Goals section ---
70
+ if (currentSection === "goals") {
71
+ const bulletMatch = trimmed.match(/^[-*]\s+(.+)$/);
72
+ if (bulletMatch) {
73
+ goals.push(bulletMatch[1].trim());
74
+ }
75
+ continue;
76
+ }
77
+
78
+ // --- User Stories section ---
79
+ if (currentSection === "user-stories") {
80
+ // Detect ### US-xxx: Title
81
+ const storyMatch = trimmed.match(/^###\s+(US-\d+):\s+(.+)$/);
82
+ if (storyMatch) {
83
+ // Flush previous story
84
+ if (currentStory) {
85
+ userStories.push(currentStory);
86
+ }
87
+ currentStory = {
88
+ id: storyMatch[1],
89
+ title: storyMatch[2].trim(),
90
+ description: "",
91
+ acceptanceCriteria: [],
92
+ };
93
+ inAcceptanceCriteria = false;
94
+ continue;
95
+ }
96
+
97
+ if (!currentStory) continue;
98
+
99
+ // Detect **Acceptance Criteria:**
100
+ if (/\*\*Acceptance Criteria:\*\*/i.test(trimmed)) {
101
+ inAcceptanceCriteria = true;
102
+ continue;
103
+ }
104
+
105
+ if (inAcceptanceCriteria) {
106
+ // Acceptance criteria items: - [ ] text or - [x] text
107
+ const acMatch = trimmed.match(/^[-*]\s+\[[ x]?\]\s+(.+)$/i);
108
+ if (acMatch) {
109
+ const acIndex = currentStory.acceptanceCriteria.length + 1;
110
+ currentStory.acceptanceCriteria.push({
111
+ id: `${currentStory.id}-AC${String(acIndex).padStart(2, "0")}`,
112
+ text: acMatch[1].trim(),
113
+ });
114
+ }
115
+ continue;
116
+ }
117
+
118
+ // Story description lines (between **As a** ... and **Acceptance Criteria:**)
119
+ if (trimmed.length > 0) {
120
+ // Strip bold markers for cleaner description
121
+ const descLine = trimmed.replace(/\*\*/g, "");
122
+ if (currentStory.description) {
123
+ currentStory.description += " " + descLine;
124
+ } else {
125
+ currentStory.description = descLine;
126
+ }
127
+ }
128
+ continue;
129
+ }
130
+
131
+ // --- Functional Requirements section ---
132
+ if (currentSection === "functional-requirements") {
133
+ // Accept both "- FR-1: desc" and "- **FR-1:** desc" (bold id)
134
+ const frMatch = trimmed.match(/^[-*]\s+(?:\*\*)?(FR-\d+)(?:\*\*)?:\s*(.+)$/);
135
+ if (frMatch) {
136
+ functionalRequirements.push({
137
+ id: frMatch[1],
138
+ description: frMatch[2].trim(),
139
+ });
140
+ }
141
+ continue;
142
+ }
143
+ }
144
+
145
+ // Flush last pending story
146
+ if (currentStory) {
147
+ userStories.push(currentStory);
148
+ }
149
+
150
+ return { goals, userStories, functionalRequirements };
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Main command
155
+ // ---------------------------------------------------------------------------
156
+
157
+ export async function runApproveRequirement(): Promise<void> {
158
+ const projectRoot = process.cwd();
159
+ const state = await readState(projectRoot);
160
+
161
+ // --- US-001: Validate status ---
162
+ const requirementDefinition = state.phases.define.requirement_definition;
163
+ if (requirementDefinition.status !== "in_progress") {
164
+ throw new Error(
165
+ `Cannot approve requirement from status '${requirementDefinition.status}'. Expected in_progress.`,
166
+ );
167
+ }
168
+
169
+ const requirementFile = requirementDefinition.file;
170
+ if (!requirementFile) {
171
+ throw new Error("Cannot approve requirement: define.requirement_definition.file is missing.");
172
+ }
173
+
174
+ const requirementPath = join(projectRoot, FLOW_REL_DIR, requirementFile);
175
+ if (!(await exists(requirementPath))) {
176
+ throw new Error(`Cannot approve requirement: file not found at ${requirementPath}`);
177
+ }
178
+
179
+ // --- US-002: Parse PRD markdown and generate JSON ---
180
+ const markdown = await readFile(requirementPath, "utf-8");
181
+ const prdData = parsePrd(markdown);
182
+ const prdJsonFileName = `it_${state.current_iteration}_PRD.json`;
183
+ const prdJsonRelPath = join(FLOW_REL_DIR, prdJsonFileName);
184
+
185
+ // Invoke write-json CLI to validate and write the PRD JSON
186
+ const prdJsonString = JSON.stringify(prdData);
187
+ const result =
188
+ await $`bun ${CLI_PATH} write-json --schema prd --out ${prdJsonRelPath} --data ${prdJsonString}`
189
+ .cwd(projectRoot)
190
+ .nothrow()
191
+ .quiet();
192
+
193
+ if (result.exitCode !== 0) {
194
+ const stderr = result.stderr.toString().trim();
195
+ console.error("PRD JSON generation failed. Requirement remains in_progress.");
196
+ if (stderr) {
197
+ console.error(stderr);
198
+ }
199
+ process.exitCode = 1;
200
+ return;
201
+ }
202
+
203
+ // --- US-001: Transition status only after successful JSON generation ---
204
+ requirementDefinition.status = "approved";
205
+
206
+ // --- US-002: Record PRD generation in state ---
207
+ state.phases.define.prd_generation.status = "completed";
208
+ state.phases.define.prd_generation.file = prdJsonFileName;
209
+
210
+ state.last_updated = new Date().toISOString();
211
+ state.updated_by = "nvst:approve-requirement";
212
+
213
+ await writeState(projectRoot, state);
214
+
215
+ console.log("Requirement approved.");
216
+ console.log(`PRD JSON written to ${prdJsonRelPath}`);
217
+ }
@@ -0,0 +1,193 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { TestPlanSchema } from "../../schemas/test-plan";
7
+ import { readState, writeState } from "../state";
8
+ import { parseTestPlan, runApproveTestPlan } from "./approve-test-plan";
9
+
10
+ async function createProjectRoot(): Promise<string> {
11
+ return mkdtemp(join(tmpdir(), "nvst-approve-test-plan-"));
12
+ }
13
+
14
+ async function withCwd<T>(cwd: string, fn: () => Promise<T>): Promise<T> {
15
+ const previous = process.cwd();
16
+ process.chdir(cwd);
17
+ try {
18
+ return await fn();
19
+ } finally {
20
+ process.chdir(previous);
21
+ }
22
+ }
23
+
24
+ async function seedState(
25
+ projectRoot: string,
26
+ status: "pending" | "pending_approval" | "created",
27
+ file: string | null,
28
+ ) {
29
+ await mkdir(join(projectRoot, ".agents", "flow"), { recursive: true });
30
+ await writeState(projectRoot, {
31
+ current_iteration: "000003",
32
+ current_phase: "prototype",
33
+ phases: {
34
+ define: {
35
+ requirement_definition: { status: "approved", file: "it_000003_product-requirement-document.md" },
36
+ prd_generation: { status: "completed", file: "it_000003_PRD.json" },
37
+ },
38
+ prototype: {
39
+ project_context: { status: "created", file: ".agents/PROJECT_CONTEXT.md" },
40
+ test_plan: { status, file },
41
+ tp_generation: { status: "pending", file: null },
42
+ prototype_build: { status: "pending", file: null },
43
+ test_execution: { status: "pending", file: null },
44
+ prototype_approved: false,
45
+ },
46
+ refactor: {
47
+ evaluation_report: { status: "pending", file: null },
48
+ refactor_plan: { status: "pending", file: null },
49
+ refactor_execution: { status: "pending", file: null },
50
+ changelog: { status: "pending", file: null },
51
+ },
52
+ },
53
+ last_updated: "2026-02-21T00:00:00.000Z",
54
+ updated_by: "seed",
55
+ history: [],
56
+ });
57
+ }
58
+
59
+ const createdRoots: string[] = [];
60
+
61
+ afterEach(async () => {
62
+ await Promise.all(createdRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
63
+ });
64
+
65
+ describe("approve test-plan command", () => {
66
+ test("registers approve test-plan command in CLI dispatch", async () => {
67
+ const source = await readFile(join(process.cwd(), "src", "cli.ts"), "utf8");
68
+
69
+ expect(source).toContain('import { runApproveTestPlan } from "./commands/approve-test-plan";');
70
+ expect(source).toContain('if (subcommand === "test-plan") {');
71
+ expect(source).toContain("await runApproveTestPlan();");
72
+ });
73
+
74
+ test("requires test_plan.status to be pending_approval", async () => {
75
+ const projectRoot = await createProjectRoot();
76
+ createdRoots.push(projectRoot);
77
+ await seedState(projectRoot, "pending", "it_000003_test-plan.md");
78
+
79
+ await withCwd(projectRoot, async () => {
80
+ await expect(runApproveTestPlan()).rejects.toThrow(
81
+ "Cannot approve test plan from status 'pending'. Expected pending_approval.",
82
+ );
83
+ });
84
+ });
85
+
86
+ test("approves test plan, generates TP JSON via write-json, and updates state fields", async () => {
87
+ const projectRoot = await createProjectRoot();
88
+ createdRoots.push(projectRoot);
89
+ await seedState(projectRoot, "pending_approval", "it_000003_test-plan.md");
90
+
91
+ const testPlanPath = join(projectRoot, ".agents", "flow", "it_000003_test-plan.md");
92
+ await writeFile(
93
+ testPlanPath,
94
+ [
95
+ "# Test plan - Iteration 000003",
96
+ "| Test Case ID | Description | Type (unit/integration/e2e) | Mode (automated/manual) | Correlated Requirements (US-XXX, FR-X) | Expected Result |",
97
+ "|---|---|---|---|---|---|",
98
+ "| TC-US001-01 | Validate login success | integration | automated | US-001, FR-1 | Login succeeds with valid credentials |",
99
+ "| TC-US001-02 | Validate login error handling | integration | manual | US-001, FR-2 | Login error is shown for invalid credentials |",
100
+ "## Scope",
101
+ "- Validate login",
102
+ "## Environment and data",
103
+ "- staging + seeded user",
104
+ ].join("\n"),
105
+ "utf8",
106
+ );
107
+
108
+ let capturedSchema = "";
109
+ let capturedOutPath = "";
110
+ let capturedPayload = "";
111
+
112
+ await withCwd(projectRoot, async () => {
113
+ await runApproveTestPlan({
114
+ invokeWriteJsonFn: async (_root, schemaName, outPath, data) => {
115
+ capturedSchema = schemaName;
116
+ capturedOutPath = outPath;
117
+ capturedPayload = data;
118
+ await writeFile(join(projectRoot, outPath), data, "utf8");
119
+ return { exitCode: 0, stderr: "" };
120
+ },
121
+ nowFn: () => new Date("2026-02-21T05:00:00.000Z"),
122
+ });
123
+ });
124
+
125
+ expect(capturedSchema).toBe("test-plan");
126
+ expect(capturedOutPath).toBe(".agents/flow/it_000003_TP.json");
127
+
128
+ const parsedPayload = JSON.parse(capturedPayload) as unknown;
129
+ const validation = TestPlanSchema.safeParse(parsedPayload);
130
+ expect(validation.success).toBe(true);
131
+ if (!validation.success) {
132
+ throw new Error("Expected test-plan payload to be valid");
133
+ }
134
+ expect(validation.data.overallStatus).toBe("pending");
135
+ expect(validation.data.scope).toEqual(["Validate login"]);
136
+ expect(validation.data.automatedTests).toEqual([
137
+ {
138
+ id: "TC-US001-01",
139
+ description: "Validate login success",
140
+ status: "pending",
141
+ correlatedRequirements: ["US-001", "FR-1"],
142
+ },
143
+ ]);
144
+ expect(validation.data.exploratoryManualTests).toEqual([
145
+ {
146
+ id: "TC-US001-02",
147
+ description: "Validate login error handling",
148
+ status: "pending",
149
+ correlatedRequirements: ["US-001", "FR-2"],
150
+ },
151
+ ]);
152
+ expect(validation.data.environmentData).toEqual(["staging + seeded user"]);
153
+
154
+ const state = await readState(projectRoot);
155
+ expect(state.phases.prototype.test_plan.status).toBe("created");
156
+ expect(state.phases.prototype.tp_generation.status).toBe("created");
157
+ expect(state.phases.prototype.tp_generation.file).toBe("it_000003_TP.json");
158
+ expect(state.last_updated).toBe("2026-02-21T05:00:00.000Z");
159
+ expect(state.updated_by).toBe("nvst:approve-test-plan");
160
+ });
161
+
162
+ test("parseTestPlan extracts IDs, correlated requirements, and defaults statuses to pending", () => {
163
+ const parsed = parseTestPlan(
164
+ [
165
+ "# Test plan - Iteration 000004",
166
+ "| Test Case ID | Description | Type (unit/integration/e2e) | Mode (automated/manual) | Correlated Requirements (US-XXX, FR-X) | Expected Result |",
167
+ "|---|---|---|---|---|---|",
168
+ "| TC-US004-01 | Parser maps automated test | unit | automated | us-004, FR-7, INVALID | Entry is captured |",
169
+ "| TC-US004-02 | Parser maps manual test | integration | manual | FR-8, us-004 | Entry is captured |",
170
+ "## Scope",
171
+ "- Parser migration",
172
+ "## Environment and data",
173
+ "- local fixtures",
174
+ ].join("\n"),
175
+ );
176
+
177
+ expect(parsed.overallStatus).toBe("pending");
178
+ expect(parsed.automatedTests[0]).toEqual({
179
+ id: "TC-US004-01",
180
+ description: "Parser maps automated test",
181
+ status: "pending",
182
+ correlatedRequirements: ["US-004", "FR-7"],
183
+ });
184
+ expect(parsed.exploratoryManualTests[0]).toEqual({
185
+ id: "TC-US004-02",
186
+ description: "Parser maps manual test",
187
+ status: "pending",
188
+ correlatedRequirements: ["FR-8", "US-004"],
189
+ });
190
+ const validation = TestPlanSchema.safeParse(parsed);
191
+ expect(validation.success).toBe(true);
192
+ });
193
+ });
@@ -0,0 +1,202 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { $ } from "bun";
4
+
5
+ import { CLI_PATH } from "../cli-path";
6
+ import type { TestPlan } from "../../schemas/test-plan";
7
+ import { exists, FLOW_REL_DIR, readState, writeState } from "../state";
8
+
9
+ interface WriteJsonResult {
10
+ exitCode: number;
11
+ stderr: string;
12
+ }
13
+
14
+ interface ApproveTestPlanDeps {
15
+ existsFn: (path: string) => Promise<boolean>;
16
+ invokeWriteJsonFn: (
17
+ projectRoot: string,
18
+ schemaName: string,
19
+ outPath: string,
20
+ data: string,
21
+ ) => Promise<WriteJsonResult>;
22
+ nowFn: () => Date;
23
+ readFileFn: typeof readFile;
24
+ }
25
+
26
+ const defaultDeps: ApproveTestPlanDeps = {
27
+ existsFn: exists,
28
+ invokeWriteJsonFn: runWriteJsonCommand,
29
+ nowFn: () => new Date(),
30
+ readFileFn: readFile,
31
+ };
32
+
33
+ export function parseTestPlan(markdown: string): TestPlan {
34
+ const scope: string[] = [];
35
+ const environmentData: string[] = [];
36
+ const automatedTests: TestPlan["automatedTests"] = [];
37
+ const exploratoryManualTests: TestPlan["exploratoryManualTests"] = [];
38
+
39
+ type Section = "scope" | "environmentData" | null;
40
+ let currentSection: Section = null;
41
+ let inTable = false;
42
+
43
+ const parseRequirements = (cell: string): string[] =>
44
+ cell
45
+ .split(",")
46
+ .map((entry) => entry.trim())
47
+ .filter((entry) => /^(US-\d{3}|FR-\d+)$/i.test(entry))
48
+ .map((entry) => entry.toUpperCase());
49
+
50
+ const lines = markdown.split("\n");
51
+
52
+ for (const line of lines) {
53
+ const trimmed = line.trim();
54
+
55
+ if (/^##\s+Scope$/i.test(trimmed)) {
56
+ currentSection = "scope";
57
+ inTable = false;
58
+ continue;
59
+ }
60
+ if (/^##\s+Environment\s*(?:and|&)\s*data$/i.test(trimmed)) {
61
+ currentSection = "environmentData";
62
+ inTable = false;
63
+ continue;
64
+ }
65
+
66
+ if (
67
+ trimmed.startsWith("|")
68
+ && trimmed.includes("Test Case ID")
69
+ && trimmed.includes("Correlated Requirements")
70
+ ) {
71
+ inTable = true;
72
+ currentSection = null;
73
+ continue;
74
+ }
75
+
76
+ if (inTable && trimmed.startsWith("|")) {
77
+ if (trimmed.includes("---|")) continue;
78
+
79
+ const cells = trimmed
80
+ .split("|")
81
+ .map((c) => c.trim())
82
+ .filter((c, i, a) => i > 0 && i < a.length - 1);
83
+
84
+ if (cells.length >= 6) {
85
+ const [id, description, , mode, correlatedRequirementsCell] = cells;
86
+ if (id === "Test Case ID") continue;
87
+
88
+ const item = {
89
+ id,
90
+ description,
91
+ status: "pending" as const,
92
+ correlatedRequirements: parseRequirements(correlatedRequirementsCell),
93
+ };
94
+ if (mode.toLowerCase().includes("automated")) {
95
+ automatedTests.push(item);
96
+ } else {
97
+ exploratoryManualTests.push(item);
98
+ }
99
+ }
100
+ continue;
101
+ }
102
+
103
+ if (inTable && trimmed.length === 0) {
104
+ inTable = false;
105
+ continue;
106
+ }
107
+
108
+ if (!currentSection || trimmed.length === 0 || /^<!--/.test(trimmed)) {
109
+ continue;
110
+ }
111
+
112
+ const bulletMatch = trimmed.match(/^[-*]\s+(.+)$/);
113
+ const value = bulletMatch ? bulletMatch[1].trim() : trimmed;
114
+ if (!value) continue;
115
+
116
+ if (currentSection === "scope") scope.push(value);
117
+ if (currentSection === "environmentData") environmentData.push(value);
118
+ }
119
+
120
+ return {
121
+ overallStatus: "pending",
122
+ scope,
123
+ environmentData,
124
+ automatedTests,
125
+ exploratoryManualTests,
126
+ };
127
+ }
128
+
129
+ async function runWriteJsonCommand(
130
+ projectRoot: string,
131
+ schemaName: string,
132
+ outPath: string,
133
+ data: string,
134
+ ): Promise<WriteJsonResult> {
135
+ const result =
136
+ await $`bun ${CLI_PATH} write-json --schema ${schemaName} --out ${outPath} --data ${data}`
137
+ .cwd(projectRoot)
138
+ .nothrow()
139
+ .quiet();
140
+
141
+ return {
142
+ exitCode: result.exitCode,
143
+ stderr: result.stderr.toString().trim(),
144
+ };
145
+ }
146
+
147
+ export async function runApproveTestPlan(
148
+ deps: Partial<ApproveTestPlanDeps> = {},
149
+ ): Promise<void> {
150
+ const projectRoot = process.cwd();
151
+ const state = await readState(projectRoot);
152
+ const mergedDeps: ApproveTestPlanDeps = { ...defaultDeps, ...deps };
153
+
154
+ const testPlan = state.phases.prototype.test_plan;
155
+ if (testPlan.status !== "pending_approval") {
156
+ throw new Error(
157
+ `Cannot approve test plan from status '${testPlan.status}'. Expected pending_approval.`,
158
+ );
159
+ }
160
+
161
+ const testPlanFile = testPlan.file;
162
+ if (!testPlanFile) {
163
+ throw new Error("Cannot approve test plan: prototype.test_plan.file is missing.");
164
+ }
165
+
166
+ const testPlanPath = join(projectRoot, FLOW_REL_DIR, testPlanFile);
167
+ if (!(await mergedDeps.existsFn(testPlanPath))) {
168
+ throw new Error(`Cannot approve test plan: file not found at ${testPlanPath}`);
169
+ }
170
+
171
+ const markdown = await mergedDeps.readFileFn(testPlanPath, "utf-8");
172
+ const tpData = parseTestPlan(markdown);
173
+ const tpJsonFileName = `it_${state.current_iteration}_TP.json`;
174
+ const tpJsonRelPath = join(FLOW_REL_DIR, tpJsonFileName);
175
+
176
+ const writeResult = await mergedDeps.invokeWriteJsonFn(
177
+ projectRoot,
178
+ "test-plan",
179
+ tpJsonRelPath,
180
+ JSON.stringify(tpData),
181
+ );
182
+
183
+ if (writeResult.exitCode !== 0) {
184
+ console.error("Test-plan JSON generation failed. Test plan remains pending_approval.");
185
+ if (writeResult.stderr) {
186
+ console.error(writeResult.stderr);
187
+ }
188
+ process.exitCode = 1;
189
+ return;
190
+ }
191
+
192
+ testPlan.status = "created";
193
+ state.phases.prototype.tp_generation.status = "created";
194
+ state.phases.prototype.tp_generation.file = tpJsonFileName;
195
+ state.last_updated = mergedDeps.nowFn().toISOString();
196
+ state.updated_by = "nvst:approve-test-plan";
197
+
198
+ await writeState(projectRoot, state);
199
+
200
+ console.log("Test plan approved.");
201
+ console.log(`Test-plan JSON written to ${tpJsonRelPath}`);
202
+ }