@abianbiya/specflow 0.1.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.
@@ -0,0 +1,33 @@
1
+ # Phase 1: Requirements
2
+
3
+ Use project context from `.specflow/project.md`; if missing, follow [project setup](project-setup.md). Create or update `requirements.md` in the active feature directory defined in [SKILL.md](../SKILL.md#directory-layout). Consult related completed specs when relevant, resolving status through [Lifecycle](lifecycle-phase.md#status-and-legacy-layout). Link to their actual paths (`../{feature-name}/` in the flat layout).
4
+
5
+ Generate an initial draft from the idea without a preliminary interview unless grilling is requested. Identify assumptions and unresolved information rather than silently inventing scope.
6
+
7
+ ## Grilling (Optional)
8
+
9
+ When requested, surface contextual questions whose answers would materially change the spec: ambiguities, concurrency, edge cases, hidden decisions, or integration constraints. Present one block, each question paired with a recommendation and brief reasoning. Wait for answers before drafting; use the confirmed intent.
10
+
11
+ ## Document Content
12
+
13
+ - Introduction: what, why, intended users, and scope.
14
+ - Numbered requirements, each with a descriptive name, a user story (As a role, I want a capability, so that a benefit), and numbered acceptance criteria. Optional notes capture constraints or dependencies.
15
+ - Out of Scope, Assumptions, and Dependencies sections.
16
+
17
+ Use stable references: `1` identifies Requirement 1; `1.2` identifies its second acceptance criterion. Preserve existing IDs when revising. Design and tasks use these same references.
18
+
19
+ ## EARS Acceptance Criteria
20
+
21
+ Write specific, measurable outcomes with one behavior per criterion and unambiguous conditions.
22
+
23
+ | Pattern | Form |
24
+ |---------|------|
25
+ | Ubiquitous | The system SHALL [behavior] |
26
+ | Event-driven | WHEN [event] THEN the system SHALL [behavior] |
27
+ | Conditional | IF [condition] THEN the system SHALL [behavior] |
28
+ | Combined | WHEN [event] AND IF [condition] THEN the system SHALL [behavior] |
29
+ | Optional | WHERE [feature included] the system SHALL [behavior] |
30
+
31
+ Cover functional behavior, relevant performance/security/accessibility constraints, error and boundary cases, and success criteria. Keep scope explicit; propose a separate spec for unrelated expansion.
32
+
33
+ Present the draft with a coverage summary and apply the [planning approval gate](../SKILL.md#core-contract).
@@ -0,0 +1,35 @@
1
+ # Phase 3: Tasks
2
+
3
+ Use approved requirements and design to create or update `tasks.md` in the active feature directory. Follow the [planning approval gate](../SKILL.md#core-contract).
4
+
5
+ Initialize or preserve [lifecycle metadata](lifecycle-phase.md#metadata) when writing the plan.
6
+
7
+ ## Plan Contract
8
+
9
+ Include a short implementation overview and groups of incremental coding tasks. Use at most two hierarchy levels: top-level groups and decimal task IDs such as `1.1`, `1.2`, `2.1`.
10
+
11
+ Each task has a checkbox, unique number, action title, scope sub-bullets, and explicit [requirement/criterion references](requirements-phase.md#document-content). Enumerate IDs instead of using `All`, so task context and validation are bounded. Include relevant design section names in the scope when they are not evident from traceability.
12
+
13
+ ```markdown
14
+ ## 1. [Work group]
15
+
16
+ - [ ] 1.1 [One logical implementation outcome]
17
+ - Depends on: none
18
+ - [Code to create or modify and integration points]
19
+ - [Tests or verification required by the design]
20
+ - _Requirements: 1.1, 1.2_
21
+ ```
22
+
23
+ Tasks must be specific, testable, traceable, and small enough to validate individually. Order dependencies before dependent work, integrate code rather than leaving orphaned components, and include testing with test-driven development where appropriate.
24
+
25
+ ## Dependency Flow
26
+
27
+ Every task declares `Depends on: none` or explicit prerequisite task IDs. These edges define the execution flow; list prerequisites before dependents and reject missing IDs, self-dependencies, and cycles before seeking plan approval. Dependencies refer to validated, integrated outcomes, not merely work started.
28
+
29
+ For example, `1.1: none`, `1.2: 1.1`, `1.3: 1.1`, `1.4: 1.2, 1.3` means 1.1 first, then 1.2 and 1.3, then 1.4. Tasks 1.2 and 1.3 are dependency-independent and candidates for parallel execution under the [execution rules](execution-phase.md#parallel-execution-optional). Avoid maintaining a second ordering diagram.
30
+
31
+ For older plans without dependency fields, retain listed sequential order and infer prerequisites conservatively from scope; missing fields do not mean independence. Clarify uncertainty before execution and add explicit dependencies through plan review before parallelizing.
32
+
33
+ Exclude deployment, human acceptance/manual E2E testing, post-deployment metrics gathering, training, business-process changes, and marketing. Documentation tasks are allowed only when explicitly requested.
34
+
35
+ Present the plan with its groups and task count for review. After approval, route execution requests to [Phase 4](execution-phase.md); execution scope and stopping rules live there and in the root contract.
@@ -0,0 +1,57 @@
1
+ # Project Context
2
+
3
+ ## Tech Stack
4
+
5
+ - **Language:** [e.g., TypeScript, Python, Java]
6
+ - **Framework:** [e.g., Next.js, FastAPI, Spring Boot]
7
+ - **Database:** [e.g., PostgreSQL, MongoDB, MySQL]
8
+ - **Testing:** [e.g., Jest, pytest, JUnit]
9
+ - **Build Tools:** [e.g., Vite, Webpack, Maven]
10
+
11
+ ## Coding Conventions
12
+
13
+ - **Naming:** [e.g., camelCase for variables, PascalCase for classes]
14
+ - **File Organization:** [e.g., feature-based folders, separation of concerns]
15
+ - **Code Style:** [e.g., ESLint config, Prettier settings, PEP 8]
16
+ - **Comments:** [e.g., JSDoc for public APIs, inline for complex logic]
17
+ - **Error Handling:** [e.g., custom error classes, consistent error responses]
18
+
19
+ ## Architectural Patterns
20
+
21
+ - **Architecture Style:** [e.g., Layered, Clean Architecture, Microservices]
22
+ - **Design Patterns:** [e.g., Repository pattern, Factory pattern, Observer]
23
+ - **State Management:** [e.g., Redux, Context API, Vuex]
24
+ - **API Design:** [e.g., RESTful, GraphQL, gRPC]
25
+ - **Data Flow:** [e.g., unidirectional data flow, event-driven]
26
+
27
+ ## Testing Standards
28
+
29
+ - **Unit Tests:** [e.g., minimum 80% coverage, test pure functions]
30
+ - **Integration Tests:** [e.g., test API endpoints, database interactions]
31
+ - **Property-Based Tests:** [e.g., use fast-check/Hypothesis for core logic]
32
+ - **Test Organization:** [e.g., co-located with source, separate test directory]
33
+ - **Mocking Strategy:** [e.g., mock external services, use test databases]
34
+
35
+ ## Security Practices
36
+
37
+ - **Authentication:** [e.g., JWT, OAuth 2.0, session-based]
38
+ - **Authorization:** [e.g., RBAC, ABAC, permission middleware]
39
+ - **Data Validation:** [e.g., input sanitization, schema validation]
40
+ - **Secrets Management:** [e.g., environment variables, vault services]
41
+
42
+ ## Performance Guidelines
43
+
44
+ - **Optimization:** [e.g., lazy loading, code splitting, caching strategies]
45
+ - **Database:** [e.g., indexing strategy, query optimization, connection pooling]
46
+ - **Monitoring:** [e.g., logging standards, metrics collection, alerting]
47
+
48
+ ## Related Documentation
49
+
50
+ - [Link to architecture docs]
51
+ - [Link to API documentation]
52
+ - [Link to deployment guides]
53
+ - [Link to team wiki]
54
+
55
+ ---
56
+
57
+ **Instructions:** Fill in the sections above with your project-specific details. This file will be read by the AI assistant when creating and executing specs to ensure consistency across all features.
@@ -0,0 +1,195 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { mkdtemp, mkdir, writeFile, rm, rename, chmod } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { SpecflowController } from "./controller.js";
6
+
7
+ let root: string;
8
+ let dir: string;
9
+
10
+ /** Fixture writer: specs/<feature>/<file> relative to the specflow dir. */
11
+ async function writeSpec(rel: string, content: string): Promise<string> {
12
+ const path = join(dir, rel);
13
+ await mkdir(join(path, ".."), { recursive: true });
14
+ await writeFile(path, content);
15
+ return path;
16
+ }
17
+
18
+ const TASKS_ACTIVE = ["---", "status: active", "---", "", "## Tasks", "", "- [ ] 1. Do the thing", " - Criteria: AC1", "- [x] 2. Done thing", ""].join("\n");
19
+ const REQUIREMENTS = "# Feature\n\n## Requirements\n\n- AC1: it works\n";
20
+
21
+ function harness(intervalMs = 25) {
22
+ const updates: number[] = [];
23
+ const diagnostics: string[] = [];
24
+ const controller = new SpecflowController(dir, {
25
+ onUpdate: () => updates.push(updates.length + 1),
26
+ onDiagnostic: (m) => diagnostics.push(m),
27
+ }, intervalMs);
28
+ return { controller, updates, diagnostics };
29
+ }
30
+
31
+ const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
32
+
33
+ beforeEach(async () => {
34
+ root = await mkdtemp(join(tmpdir(), "specflow-ctl-"));
35
+ dir = join(root, ".specflow");
36
+ });
37
+
38
+ afterEach(async () => {
39
+ await rm(root, { recursive: true, force: true });
40
+ });
41
+
42
+ describe("SpecflowController.scan", () => {
43
+ test("a missing .specflow directory is not an error and yields no specs", async () => {
44
+ const { controller, diagnostics } = harness();
45
+ await controller.scan();
46
+ expect(controller.specs).toEqual([]);
47
+ expect(controller.active()).toBeUndefined();
48
+ expect(diagnostics).toEqual([]);
49
+ });
50
+
51
+ test("discovers flat and legacy specs and exposes the ranking winner", async () => {
52
+ await writeSpec("specs/active-flat/tasks.md", TASKS_ACTIVE);
53
+ // legacy layout: no frontmatter, status inferred from the containing directory
54
+ await writeSpec("specs/archived/older/tasks.md", "# Old\n");
55
+ const { controller } = harness();
56
+ await controller.scan();
57
+ expect(controller.specs.map((s) => s.name).sort()).toEqual(["active-flat", "older"]);
58
+ expect(controller.specs.find((s) => s.name === "older")?.status).toBe("archived");
59
+ // archived ranks below in-progress even though it is the newest on disk
60
+ expect(controller.active()?.name).toBe("active-flat");
61
+ });
62
+
63
+ test("a requirements-only spec is eligible", async () => {
64
+ await writeSpec("specs/req-only/requirements.md", REQUIREMENTS);
65
+ const { controller } = harness();
66
+ await controller.scan();
67
+ expect(controller.specs.map((s) => s.name)).toEqual(["req-only"]);
68
+ });
69
+
70
+ test("an unreadable tasks.md is reported once as a diagnostic, never thrown", async () => {
71
+ const path = await writeSpec("specs/broken/tasks.md", TASKS_ACTIVE);
72
+ await chmod(path, 0o000);
73
+ const { controller, diagnostics } = harness();
74
+ await controller.scan();
75
+ await controller.scan();
76
+ expect(controller.specs.map((s) => s.name)).toEqual(["broken"]);
77
+ expect(diagnostics.length).toBe(1); // reported once, not once per scan
78
+ });
79
+
80
+ test("an unreadable non-task document still lists the spec without a diagnostic", async () => {
81
+ // Discovery only needs tasks.md's content; requirements.md is read by the
82
+ // viewer, which reports the failure on open (AC4) instead of on every poll.
83
+ const path = await writeSpec("specs/quiet/requirements.md", REQUIREMENTS);
84
+ await writeSpec("specs/quiet/tasks.md", TASKS_ACTIVE);
85
+ await chmod(path, 0o000);
86
+ const { controller, diagnostics } = harness();
87
+ await controller.scan();
88
+ expect(controller.specs.map((s) => s.name)).toEqual(["quiet"]);
89
+ expect(diagnostics).toEqual([]);
90
+ });
91
+ });
92
+
93
+ describe("SpecflowController pinning", () => {
94
+ test("a pin survives refreshes but clears when the spec disappears", async () => {
95
+ await writeSpec("specs/one/tasks.md", TASKS_ACTIVE);
96
+ await writeSpec("specs/two/tasks.md", TASKS_ACTIVE);
97
+ const { controller } = harness();
98
+ await controller.scan();
99
+ const two = controller.specs.find((s) => s.name === "two");
100
+ controller.pin(two!.dir);
101
+ await controller.scan();
102
+ expect(controller.active()?.name).toBe("two");
103
+
104
+ await rm(join(dir, "specs/two"), { recursive: true, force: true });
105
+ await controller.scan();
106
+ expect(controller.pinned).toBeUndefined();
107
+ expect(controller.active()?.name).toBe("one");
108
+ });
109
+
110
+ test("hide and show are session-scoped state changes", async () => {
111
+ await writeSpec("specs/one/tasks.md", TASKS_ACTIVE);
112
+ const { controller } = harness();
113
+ await controller.scan();
114
+ expect(controller.hidden).toBe(false);
115
+ controller.hide();
116
+ expect(controller.hidden).toBe(true);
117
+ controller.show();
118
+ expect(controller.hidden).toBe(false);
119
+ });
120
+ });
121
+
122
+ describe("SpecflowController.onUpdate", () => {
123
+ test("fires only when the rendered content changes", async () => {
124
+ await writeSpec("specs/one/tasks.md", TASKS_ACTIVE);
125
+ const { controller, updates } = harness();
126
+ await controller.scan();
127
+ const afterFirst = updates.length;
128
+ await controller.scan(); // identical content
129
+ expect(updates.length).toBe(afterFirst);
130
+ controller.hide(); // visibility is part of the signature
131
+ expect(updates.length).toBe(afterFirst + 1);
132
+ });
133
+
134
+ test("fires when only the gate changes", async () => {
135
+ await writeSpec("specs/one/tasks.md", TASKS_ACTIVE);
136
+ const { controller, updates } = harness();
137
+ await controller.scan();
138
+ const before = updates.length;
139
+ await writeSpec("specs/one/tasks.md", TASKS_ACTIVE.replace("status: active", "status: active\ngate: review"));
140
+ await controller.scan();
141
+ expect(controller.active()?.gate).toBe("review");
142
+ expect(updates.length).toBe(before + 1);
143
+ });
144
+ });
145
+
146
+ describe("SpecflowController polling (AC6)", () => {
147
+ test("reflects a created spec, an atomic replace, and a deletion within a second", async () => {
148
+ const { controller } = harness(25);
149
+ await controller.scan();
150
+ controller.start();
151
+ try {
152
+ await writeSpec("specs/new/tasks.md", TASKS_ACTIVE);
153
+ await wait(400);
154
+ expect(controller.specs.map((s) => s.name)).toEqual(["new"]);
155
+
156
+ // atomic replace (write temp + rename over the target)
157
+ const target = join(dir, "specs/new/tasks.md");
158
+ const tmp = `${target}.tmp`;
159
+ await writeFile(tmp, TASKS_ACTIVE.replace("- [ ] 1. Do the thing", "- [x] 1. Do the thing"));
160
+ await rename(tmp, target);
161
+ await wait(400);
162
+ expect(controller.active()?.done).toBe(2);
163
+
164
+ await rm(join(dir, "specs/new/tasks.md"), { force: true });
165
+ await controller.scan();
166
+ expect(controller.specs).toEqual([]);
167
+ } finally {
168
+ controller.stop();
169
+ }
170
+ });
171
+
172
+ test("stop() halts polling and clears session state", async () => {
173
+ await writeSpec("specs/one/tasks.md", TASKS_ACTIVE);
174
+ const { controller } = harness(25);
175
+ await controller.scan();
176
+ controller.start();
177
+ await wait(60);
178
+ controller.stop();
179
+ expect(controller.specs).toEqual([]);
180
+ expect(controller.pinned).toBeUndefined();
181
+
182
+ await writeSpec("specs/late/tasks.md", TASKS_ACTIVE);
183
+ await wait(200);
184
+ expect(controller.specs).toEqual([]); // no ticks after stop
185
+ });
186
+
187
+ test("a scan started before stop() cannot apply its result", async () => {
188
+ await writeSpec("specs/one/tasks.md", TASKS_ACTIVE);
189
+ const { controller } = harness();
190
+ const pending = controller.scan();
191
+ controller.stop();
192
+ await pending;
193
+ expect(controller.specs).toEqual([]);
194
+ });
195
+ });
@@ -0,0 +1,119 @@
1
+ /**
2
+ * controller.ts — session-scoped state for the specflow panel: discovery
3
+ * polling, pinned selection, change/diagnostic dedup, and shutdown
4
+ * invalidation. Declared fork of speclet-tui/src/controller.ts (AC7);
5
+ * filesystem-free except through discoverSpecflows.
6
+ */
7
+
8
+ import { discoverSpecflows, type SpecflowSpec } from "./parse.js";
9
+ import { selectActive } from "./render.js";
10
+
11
+ export interface ControllerHooks {
12
+ /** The rendered content of the active spec changed (or appeared/disappeared). */
13
+ onUpdate(): void;
14
+ /** A deduplicated diagnostic to surface (directory or read failure). */
15
+ onDiagnostic(message: string): void;
16
+ }
17
+
18
+ export class SpecflowController {
19
+ specs: SpecflowSpec[] = [];
20
+ /** Spec directory pinned via /specflow; cleared automatically when it disappears. */
21
+ pinned: string | undefined;
22
+ /** Panel visibility, toggled from the /specflow picker. Session-scoped. */
23
+ hidden = false;
24
+
25
+ private timer: ReturnType<typeof setTimeout> | undefined;
26
+ private generation = 0;
27
+ private lastSignature: string | undefined;
28
+ private lastDiagnostic: string | undefined;
29
+
30
+ constructor(
31
+ private readonly specflowDir: string,
32
+ private readonly hooks: ControllerHooks,
33
+ private readonly intervalMs = 500,
34
+ ) {}
35
+
36
+ active(): SpecflowSpec | undefined {
37
+ return selectActive(this.specs, this.pinned);
38
+ }
39
+
40
+ pin(dir: string | undefined): void {
41
+ this.pinned = dir;
42
+ this.refreshSnapshot();
43
+ }
44
+
45
+ hide(): void {
46
+ this.hidden = true;
47
+ this.refreshSnapshot();
48
+ }
49
+
50
+ show(): void {
51
+ this.hidden = false;
52
+ this.refreshSnapshot();
53
+ }
54
+
55
+ /** Scan once now; safe to call directly (used by tests and start()). */
56
+ async scan(): Promise<void> {
57
+ const gen = this.generation;
58
+ const { specs, dirError } = await discoverSpecflows(this.specflowDir);
59
+ if (gen !== this.generation) return; // stale scan after shutdown
60
+
61
+ if (this.pinned !== undefined && !specs.some((s) => s.dir === this.pinned)) {
62
+ this.pinned = undefined; // pinned spec disappeared — back to ranking (AC1)
63
+ }
64
+ this.specs = specs;
65
+
66
+ const diagnostic =
67
+ dirError ?? (specs.filter((s) => s.error).map((s) => `${s.name}: ${s.error}`).join("; ") || undefined);
68
+ if (diagnostic !== this.lastDiagnostic) {
69
+ this.lastDiagnostic = diagnostic;
70
+ if (diagnostic) this.hooks.onDiagnostic(diagnostic);
71
+ }
72
+ this.refreshSnapshot();
73
+ }
74
+
75
+ /** Start the non-overlapping poll loop (AC6). */
76
+ start(): void {
77
+ void this.loop();
78
+ }
79
+
80
+ stop(): void {
81
+ this.generation++; // invalidates in-flight scans and scheduled ticks
82
+ if (this.timer) clearTimeout(this.timer);
83
+ this.timer = undefined;
84
+ this.specs = [];
85
+ this.pinned = undefined;
86
+ this.hidden = false;
87
+ }
88
+
89
+ private async loop(): Promise<void> {
90
+ const gen = this.generation;
91
+ await this.scan();
92
+ if (gen !== this.generation) return;
93
+ this.timer = setTimeout(() => void this.loop(), this.intervalMs);
94
+ }
95
+
96
+ /** Fire onUpdate only when the rendered content of the active spec changed. */
97
+ private refreshSnapshot(): void {
98
+ const active = this.active();
99
+ const signature = JSON.stringify([
100
+ this.hidden,
101
+ active
102
+ ? [
103
+ active.dir,
104
+ active.name,
105
+ active.status,
106
+ active.phase,
107
+ active.done,
108
+ active.total,
109
+ active.gate,
110
+ active.error ?? null,
111
+ ]
112
+ : null,
113
+ ]);
114
+ if (signature !== this.lastSignature) {
115
+ this.lastSignature = signature;
116
+ this.hooks.onUpdate();
117
+ }
118
+ }
119
+ }