@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.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/extensions/index.ts +323 -0
- package/package.json +37 -0
- package/skills/specflow/SKILL.md +48 -0
- package/skills/specflow/references/design-phase.md +27 -0
- package/skills/specflow/references/execution-phase.md +55 -0
- package/skills/specflow/references/lifecycle-phase.md +57 -0
- package/skills/specflow/references/project-setup.md +9 -0
- package/skills/specflow/references/requirements-phase.md +33 -0
- package/skills/specflow/references/tasks-phase.md +35 -0
- package/skills/specflow/templates/project.md +57 -0
- package/src/controller.test.ts +195 -0
- package/src/controller.ts +119 -0
- package/src/parse.test.ts +487 -0
- package/src/parse.ts +389 -0
- package/src/render.test.ts +395 -0
- package/src/render.ts +350 -0
- package/src/shared.ts +93 -0
- package/src/trace.test.ts +182 -0
- package/src/trace.ts +135 -0
package/src/trace.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trace.ts — pure derivations over a parsed spec: which requirements have
|
|
3
|
+
* implementing tasks, which task can run next. No filesystem, no imports
|
|
4
|
+
* except types from ./parse.js.
|
|
5
|
+
*
|
|
6
|
+
* Detail-row grammar (rows arrive control-stripped, leading bullet retained):
|
|
7
|
+
* "- Criteria: AC1" / "- Criteria: AC1, AC2" / "- Depends on: 1.1"
|
|
8
|
+
* Tolerances: optional dash/bullet, case-insensitive label, comma- and/or
|
|
9
|
+
* space-separated values, dotted ids (`1.1`, `2.3`).
|
|
10
|
+
*
|
|
11
|
+
* dependencyGraph classification (each unfinished task lands in exactly one
|
|
12
|
+
* list, priority dangling > blocked > ready):
|
|
13
|
+
* - dangling: cites a dependency id that does not exist — never treated as
|
|
14
|
+
* satisfied, never ready even if its other dependencies are done.
|
|
15
|
+
* - blocked: all dependencies exist, at least one is unfinished.
|
|
16
|
+
* - ready: all dependencies exist and are done (a task with no dependencies
|
|
17
|
+
* is ready).
|
|
18
|
+
* A task depending on ITSELF is blocked-on-itself (the id exists and the task
|
|
19
|
+
* is unfinished), never dangling. Done tasks are excluded from all lists.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { SpecflowSpec, SpecflowTask } from "./parse.js";
|
|
23
|
+
|
|
24
|
+
const CRITERIA_ROW_RE = /^(?:[-*+]\s*)?criteria\s*:\s*(.+)$/i;
|
|
25
|
+
const DEPENDS_ROW_RE = /^(?:[-*+]\s*)?depends\s+on\s*:\s*(.+)$/i;
|
|
26
|
+
|
|
27
|
+
function rowValues(details: string[] | undefined, re: RegExp): string[] {
|
|
28
|
+
const out: string[] = [];
|
|
29
|
+
for (const raw of details ?? []) {
|
|
30
|
+
const m = raw.match(re);
|
|
31
|
+
if (!m) continue;
|
|
32
|
+
for (const token of m[1].split(/[\s,;]+/)) {
|
|
33
|
+
if (token) out.push(token);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** AC ids a task cites, from its `Criteria:` detail rows. */
|
|
40
|
+
export function criteriaIdsOf(task: SpecflowTask): string[] {
|
|
41
|
+
return rowValues(task.details, CRITERIA_ROW_RE);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Dependency ids a task declares, from its `Depends on:` detail rows. */
|
|
45
|
+
export function dependsOn(task: SpecflowTask): string[] {
|
|
46
|
+
return rowValues(task.details, DEPENDS_ROW_RE);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface CriteriaLink {
|
|
50
|
+
id: string;
|
|
51
|
+
/** Task ids citing this AC, in document order. */
|
|
52
|
+
tasks: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CriteriaTrace {
|
|
56
|
+
defined: string[];
|
|
57
|
+
claimed: CriteriaLink[];
|
|
58
|
+
/** Defined in requirements.md but cited by NO task — a requirement nothing implements. */
|
|
59
|
+
unclaimed: string[];
|
|
60
|
+
/** Cited by a task but not defined in requirements.md (typo or stale citation). */
|
|
61
|
+
orphan: { id: string; taskId: string }[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Which defined criteria tasks claim. `claimed` follows `defined` order. */
|
|
65
|
+
export function traceCriteria(spec: SpecflowSpec): CriteriaTrace {
|
|
66
|
+
const defined = [...spec.criteria];
|
|
67
|
+
const definedSet = new Set(defined);
|
|
68
|
+
const byId = new Map<string, string[]>();
|
|
69
|
+
const orphan: { id: string; taskId: string }[] = [];
|
|
70
|
+
for (const task of spec.tasks) {
|
|
71
|
+
for (const id of criteriaIdsOf(task)) {
|
|
72
|
+
if (!definedSet.has(id)) {
|
|
73
|
+
orphan.push({ id, taskId: task.id });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
let tasks = byId.get(id);
|
|
77
|
+
if (!tasks) {
|
|
78
|
+
tasks = [];
|
|
79
|
+
byId.set(id, tasks);
|
|
80
|
+
}
|
|
81
|
+
if (!tasks.includes(task.id)) tasks.push(task.id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const claimed = defined
|
|
85
|
+
.filter((id) => byId.has(id))
|
|
86
|
+
.map((id) => ({ id, tasks: byId.get(id)! }));
|
|
87
|
+
return {
|
|
88
|
+
defined,
|
|
89
|
+
claimed,
|
|
90
|
+
unclaimed: defined.filter((id) => !byId.has(id)),
|
|
91
|
+
orphan,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface DependencyGraph {
|
|
96
|
+
/** Unfinished, every dependency exists AND is done. */
|
|
97
|
+
ready: string[];
|
|
98
|
+
/** Unfinished, waiting on at least one unfinished dependency. */
|
|
99
|
+
blocked: { id: string; by: string[] }[];
|
|
100
|
+
/** Depends on ids that do not exist — never treated as satisfied. */
|
|
101
|
+
dangling: { id: string; missing: string[] }[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function tasksOf(spec: SpecflowTask[] | SpecflowSpec): SpecflowTask[] {
|
|
105
|
+
return Array.isArray(spec) ? spec : spec.tasks;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Classify every unfinished task's dependencies (see module doc for the rules). */
|
|
109
|
+
export function dependencyGraph(spec: SpecflowTask[] | SpecflowSpec): DependencyGraph {
|
|
110
|
+
const tasks = tasksOf(spec);
|
|
111
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
112
|
+
const ready: string[] = [];
|
|
113
|
+
const blocked: { id: string; by: string[] }[] = [];
|
|
114
|
+
const dangling: { id: string; missing: string[] }[] = [];
|
|
115
|
+
for (const task of tasks) {
|
|
116
|
+
if (task.done) continue;
|
|
117
|
+
const deps = dependsOn(task);
|
|
118
|
+
const missing = deps.filter((d) => !byId.has(d));
|
|
119
|
+
if (missing.length > 0) {
|
|
120
|
+
dangling.push({ id: task.id, missing });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const waiting = deps.filter((d) => !byId.get(d)!.done);
|
|
124
|
+
if (waiting.length > 0) blocked.push({ id: task.id, by: waiting });
|
|
125
|
+
else ready.push(task.id);
|
|
126
|
+
}
|
|
127
|
+
return { ready, blocked, dangling };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** First READY unfinished task in document order; undefined when none. */
|
|
131
|
+
export function nextTask(spec: SpecflowTask[] | SpecflowSpec): SpecflowTask | undefined {
|
|
132
|
+
const tasks = tasksOf(spec);
|
|
133
|
+
const ready = new Set(dependencyGraph(tasks).ready);
|
|
134
|
+
return tasks.find((t) => !t.done && ready.has(t.id));
|
|
135
|
+
}
|