@schilling.mark.a/software-methodology 1.0.0 → 1.0.1

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 (69) hide show
  1. package/.github/copilot-instructions.md +159 -0
  2. package/README.md +172 -6
  3. package/docs/story-map/backbone.md +141 -0
  4. package/docs/story-map/releases/r1-walking-skeleton.md +152 -0
  5. package/docs/story-map/user-tasks/ACT-001-task-001.md +45 -0
  6. package/docs/story-map/user-tasks/ACT-001-task-002.md +48 -0
  7. package/docs/story-map/user-tasks/ACT-002-task-001.md +47 -0
  8. package/docs/story-map/user-tasks/ACT-002-task-002.md +47 -0
  9. package/docs/story-map/user-tasks/ACT-002-task-003.md +46 -0
  10. package/docs/story-map/user-tasks/ACT-003-task-001.md +47 -0
  11. package/docs/story-map/user-tasks/ACT-003-task-002.md +46 -0
  12. package/docs/story-map/user-tasks/ACT-003-task-003.md +49 -0
  13. package/docs/story-map/user-tasks/ACT-003-task-004.md +47 -0
  14. package/docs/story-map/user-tasks/ACT-004-task-001.md +48 -0
  15. package/docs/story-map/user-tasks/ACT-004-task-002.md +49 -0
  16. package/docs/story-map/user-tasks/ACT-004-task-003.md +47 -0
  17. package/docs/story-map/user-tasks/ACT-005-task-001.md +47 -0
  18. package/docs/story-map/user-tasks/ACT-005-task-002.md +48 -0
  19. package/docs/story-map/user-tasks/ACT-005-task-003.md +48 -0
  20. package/docs/story-map/user-tasks/ACT-005-task-004.md +48 -0
  21. package/docs/story-map/user-tasks/ACT-006-task-001.md +47 -0
  22. package/docs/story-map/user-tasks/ACT-006-task-002.md +46 -0
  23. package/docs/story-map/user-tasks/ACT-006-task-003.md +47 -0
  24. package/docs/story-map/user-tasks/ACT-006-task-004.md +46 -0
  25. package/docs/story-map/user-tasks/ACT-007-task-001.md +48 -0
  26. package/docs/story-map/user-tasks/ACT-007-task-002.md +47 -0
  27. package/docs/story-map/user-tasks/ACT-007-task-003.md +47 -0
  28. package/docs/story-map/user-tasks/ACT-007-task-004.md +48 -0
  29. package/docs/story-map/user-tasks/ACT-008-task-001.md +48 -0
  30. package/docs/story-map/user-tasks/ACT-008-task-002.md +48 -0
  31. package/docs/story-map/user-tasks/ACT-008-task-003.md +47 -0
  32. package/docs/story-map/user-tasks/ACT-008-task-004.md +48 -0
  33. package/docs/story-map/walking-skeleton.md +95 -0
  34. package/docs/value-proposition-canvas.md +171 -0
  35. package/features/mcp-server/query-vpc.feature +48 -0
  36. package/features/mcp-server/read-reference.feature +41 -0
  37. package/features/mcp-server/read-skill.feature +33 -0
  38. package/features/mcp-server/search-guidance.feature +42 -0
  39. package/features/mcp-server/suggest-next-step.feature +61 -0
  40. package/features/mcp-server/validate-gherkin.feature +54 -0
  41. package/mcp-server/QUICKSTART.md +172 -0
  42. package/mcp-server/README.md +171 -0
  43. package/mcp-server/dist/index.d.ts +12 -0
  44. package/mcp-server/dist/index.js +296 -0
  45. package/mcp-server/dist/repository.d.ts +59 -0
  46. package/mcp-server/dist/repository.js +211 -0
  47. package/mcp-server/dist/tools/gherkin-validator.d.ts +16 -0
  48. package/mcp-server/dist/tools/gherkin-validator.js +152 -0
  49. package/mcp-server/dist/tools/guidance-searcher.d.ts +11 -0
  50. package/mcp-server/dist/tools/guidance-searcher.js +34 -0
  51. package/mcp-server/dist/tools/next-step-suggester.d.ts +16 -0
  52. package/mcp-server/dist/tools/next-step-suggester.js +210 -0
  53. package/mcp-server/dist/tools/reference-reader.d.ts +17 -0
  54. package/mcp-server/dist/tools/reference-reader.js +57 -0
  55. package/mcp-server/dist/tools/skill-reader.d.ts +17 -0
  56. package/mcp-server/dist/tools/skill-reader.js +38 -0
  57. package/mcp-server/dist/tools/vpc-querier.d.ts +37 -0
  58. package/mcp-server/dist/tools/vpc-querier.js +158 -0
  59. package/mcp-server/package.json +42 -0
  60. package/mcp-server/src/index.ts +331 -0
  61. package/mcp-server/src/repository.ts +254 -0
  62. package/mcp-server/src/tools/gherkin-validator.ts +206 -0
  63. package/mcp-server/src/tools/guidance-searcher.ts +42 -0
  64. package/mcp-server/src/tools/next-step-suggester.ts +243 -0
  65. package/mcp-server/src/tools/reference-reader.ts +71 -0
  66. package/mcp-server/src/tools/skill-reader.ts +47 -0
  67. package/mcp-server/src/tools/vpc-querier.ts +201 -0
  68. package/mcp-server/tsconfig.json +17 -0
  69. package/package.json +8 -2
@@ -0,0 +1,254 @@
1
+ import { promises as fs } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ export interface Skill {
9
+ name: string;
10
+ displayName: string;
11
+ description: string;
12
+ path: string;
13
+ }
14
+
15
+ export interface Reference {
16
+ name: string;
17
+ path: string;
18
+ skill: string;
19
+ }
20
+
21
+ /**
22
+ * Repository for accessing methodology files
23
+ */
24
+ export class MethodologyRepository {
25
+ private methodologyRoot: string;
26
+
27
+ constructor() {
28
+ // Point to parent directory from mcp-server
29
+ this.methodologyRoot = join(__dirname, "..", "..");
30
+ }
31
+
32
+ /**
33
+ * List all available skills
34
+ */
35
+ async listSkills(): Promise<Skill[]> {
36
+ const skills = [
37
+ "product-strategy",
38
+ "ux-research",
39
+ "story-mapping",
40
+ "bdd-specification",
41
+ "ux-design",
42
+ "ui-design-workflow",
43
+ "ui-design-system",
44
+ "atdd-workflow",
45
+ "clean-code",
46
+ "cicd-pipeline",
47
+ "continuous-improvement",
48
+ ];
49
+
50
+ const skillList: Skill[] = [];
51
+
52
+ for (const skillName of skills) {
53
+ const skillPath = join(this.methodologyRoot, skillName, "SKILL.md");
54
+ try {
55
+ const content = await fs.readFile(skillPath, "utf-8");
56
+ const description = this.extractDescription(content);
57
+ skillList.push({
58
+ name: skillName,
59
+ displayName: this.toDisplayName(skillName),
60
+ description,
61
+ path: skillPath,
62
+ });
63
+ } catch (error) {
64
+ // Skip skills that don't have SKILL.md yet
65
+ console.error(`Warning: Could not read ${skillPath}`);
66
+ }
67
+ }
68
+
69
+ return skillList;
70
+ }
71
+
72
+ /**
73
+ * Read a skill's main documentation
74
+ */
75
+ async readSkill(skillName: string): Promise<string> {
76
+ const skillPath = join(this.methodologyRoot, skillName, "SKILL.md");
77
+ return await fs.readFile(skillPath, "utf-8");
78
+ }
79
+
80
+ /**
81
+ * List references for a skill
82
+ */
83
+ async listReferences(skillName: string): Promise<Reference[]> {
84
+ const referencesDir = join(this.methodologyRoot, skillName, "references");
85
+
86
+ try {
87
+ const files = await fs.readdir(referencesDir);
88
+ return files
89
+ .filter((f) => f.endsWith(".md"))
90
+ .map((f) => ({
91
+ name: f,
92
+ path: join(referencesDir, f),
93
+ skill: skillName,
94
+ }));
95
+ } catch (error) {
96
+ return []; // No references directory
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Read a reference document
102
+ */
103
+ async readReference(skillName: string, referenceName: string): Promise<string> {
104
+ const refPath = join(this.methodologyRoot, skillName, "references", referenceName);
105
+ return await fs.readFile(refPath, "utf-8");
106
+ }
107
+
108
+ /**
109
+ * Read VPC content
110
+ */
111
+ async readVPC(): Promise<string> {
112
+ const vpcPath = join(this.methodologyRoot, "docs", "value-proposition-canvas.md");
113
+ return await fs.readFile(vpcPath, "utf-8");
114
+ }
115
+
116
+ /**
117
+ * Read story map backbone
118
+ */
119
+ async readStoryMap(): Promise<string | null> {
120
+ const storyMapPath = join(this.methodologyRoot, "docs", "story-map", "backbone.md");
121
+ try {
122
+ return await fs.readFile(storyMapPath, "utf-8");
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * List feature files
130
+ */
131
+ async listFeatureFiles(projectPath?: string): Promise<string[]> {
132
+ const featuresDir = projectPath
133
+ ? join(projectPath, "features")
134
+ : join(this.methodologyRoot, "features");
135
+
136
+ try {
137
+ const files = await this.walkDirectory(featuresDir);
138
+ return files.filter((f) => f.endsWith(".feature"));
139
+ } catch {
140
+ return [];
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Search all content
146
+ */
147
+ async searchContent(query: string): Promise<Array<{ file: string; content: string; relevance: number }>> {
148
+ const results: Array<{ file: string; content: string; relevance: number }> = [];
149
+ const skills = await this.listSkills();
150
+
151
+ const queryLower = query.toLowerCase();
152
+
153
+ for (const skill of skills) {
154
+ const content = await this.readSkill(skill.name);
155
+ const relevance = this.calculateRelevance(content, queryLower);
156
+
157
+ if (relevance > 0) {
158
+ results.push({
159
+ file: `${skill.name}/SKILL.md`,
160
+ content: this.extractRelevantSection(content, queryLower),
161
+ relevance,
162
+ });
163
+ }
164
+
165
+ const references = await this.listReferences(skill.name);
166
+ for (const ref of references) {
167
+ const refContent = await this.readReference(skill.name, ref.name);
168
+ const refRelevance = this.calculateRelevance(refContent, queryLower);
169
+
170
+ if (refRelevance > 0) {
171
+ results.push({
172
+ file: `${skill.name}/references/${ref.name}`,
173
+ content: this.extractRelevantSection(refContent, queryLower),
174
+ relevance: refRelevance,
175
+ });
176
+ }
177
+ }
178
+ }
179
+
180
+ return results.sort((a, b) => b.relevance - a.relevance);
181
+ }
182
+
183
+ private async walkDirectory(dir: string): Promise<string[]> {
184
+ const files: string[] = [];
185
+ const entries = await fs.readdir(dir, { withFileTypes: true });
186
+
187
+ for (const entry of entries) {
188
+ const fullPath = join(dir, entry.name);
189
+ if (entry.isDirectory()) {
190
+ files.push(...(await this.walkDirectory(fullPath)));
191
+ } else {
192
+ files.push(fullPath);
193
+ }
194
+ }
195
+
196
+ return files;
197
+ }
198
+
199
+ private extractDescription(content: string): string {
200
+ // Extract description from frontmatter if exists
201
+ const frontmatterMatch = content.match(/---\n.*?description:\s*(.+?)\n/s);
202
+ if (frontmatterMatch) {
203
+ return frontmatterMatch[1].trim();
204
+ }
205
+
206
+ // Otherwise get first paragraph
207
+ const lines = content.split("\n");
208
+ for (const line of lines) {
209
+ if (line.trim() && !line.startsWith("#") && !line.startsWith("---")) {
210
+ return line.trim();
211
+ }
212
+ }
213
+
214
+ return "";
215
+ }
216
+
217
+ private toDisplayName(skillName: string): string {
218
+ return skillName
219
+ .split("-")
220
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
221
+ .join(" ");
222
+ }
223
+
224
+ private calculateRelevance(content: string, query: string): number {
225
+ const contentLower = content.toLowerCase();
226
+ const words = query.split(/\s+/);
227
+
228
+ let score = 0;
229
+ for (const word of words) {
230
+ const count = (contentLower.match(new RegExp(word, "g")) || []).length;
231
+ score += count;
232
+ }
233
+
234
+ return score;
235
+ }
236
+
237
+ private extractRelevantSection(content: string, query: string): string {
238
+ const lines = content.split("\n");
239
+ const queryWords = query.split(/\s+/);
240
+
241
+ for (let i = 0; i < lines.length; i++) {
242
+ const lineLower = lines[i].toLowerCase();
243
+ if (queryWords.some((word) => lineLower.includes(word))) {
244
+ // Extract context around match (5 lines before and after)
245
+ const start = Math.max(0, i - 5);
246
+ const end = Math.min(lines.length, i + 6);
247
+ return lines.slice(start, end).join("\n");
248
+ }
249
+ }
250
+
251
+ // Fallback: return first 300 chars
252
+ return content.slice(0, 300) + "...";
253
+ }
254
+ }
@@ -0,0 +1,206 @@
1
+ import { MethodologyRepository } from "../repository.js";
2
+
3
+ interface ValidationResult {
4
+ isValid: boolean;
5
+ errors: string[];
6
+ warnings: string[];
7
+ suggestions: string[];
8
+ }
9
+
10
+ export class GherkinValidator {
11
+ constructor(private repository: MethodologyRepository) {}
12
+
13
+ async validate(content: string) {
14
+ const result: ValidationResult = {
15
+ isValid: true,
16
+ errors: [],
17
+ warnings: [],
18
+ suggestions: [],
19
+ };
20
+
21
+ // Check for feature header
22
+ if (!content.includes("Feature:")) {
23
+ result.isValid = false;
24
+ result.errors.push("Missing 'Feature:' declaration");
25
+ }
26
+
27
+ // Check for user story (As a... I want... So that...)
28
+ const hasAsA = /As an?/i.test(content);
29
+ const hasIWant = /I want/i.test(content);
30
+ const hasSoThat = /So that/i.test(content);
31
+
32
+ if (!hasAsA || !hasIWant || !hasSoThat) {
33
+ result.isValid = false;
34
+ result.errors.push(
35
+ "Missing user story format (As a... I want... So that...)"
36
+ );
37
+ }
38
+
39
+ // Check for VPC traceability
40
+ const vpcKeywords = [
41
+ "business value",
42
+ "pain",
43
+ "gain",
44
+ "user need",
45
+ "customer job",
46
+ ];
47
+ const hasSomeTrace = vpcKeywords.some((keyword) =>
48
+ content.toLowerCase().includes(keyword.toLowerCase())
49
+ );
50
+
51
+ if (!hasSomeTrace) {
52
+ result.warnings.push(
53
+ "No clear VPC traceability found. Consider referencing pains/gains in 'So that' clause"
54
+ );
55
+ }
56
+
57
+ // Check Given-When-Then structure
58
+ const scenarios = this.extractScenarios(content);
59
+ for (const scenario of scenarios) {
60
+ this.validateScenarioStructure(scenario, result);
61
+ }
62
+
63
+ // Check for repeated scenarios (suggest Scenario Outline)
64
+ if (scenarios.length >= 3) {
65
+ const structures = scenarios.map((s) => this.getScenarioStructure(s));
66
+ const hasSimilar = this.hasSimilarStructures(structures);
67
+
68
+ if (hasSimilar) {
69
+ result.suggestions.push(
70
+ "Multiple scenarios have similar structure. Consider using Scenario Outline with Examples table. Reference: bdd-specification/gherkin-patterns.md"
71
+ );
72
+ }
73
+ }
74
+
75
+ // Format results
76
+ const text = this.formatValidationResults(result);
77
+
78
+ return {
79
+ content: [
80
+ {
81
+ type: "text" as const,
82
+ text,
83
+ },
84
+ ],
85
+ };
86
+ }
87
+
88
+ private extractScenarios(content: string): string[] {
89
+ const scenarios: string[] = [];
90
+ const scenarioRegex = /Scenario(?: Outline)?:(.+?)(?=\n\s*Scenario|$)/gs;
91
+
92
+ let match;
93
+ while ((match = scenarioRegex.exec(content)) !== null) {
94
+ scenarios.push(match[0]);
95
+ }
96
+
97
+ return scenarios;
98
+ }
99
+
100
+ private validateScenarioStructure(
101
+ scenario: string,
102
+ result: ValidationResult
103
+ ): void {
104
+ const lines = scenario.split("\n").map((l) => l.trim());
105
+
106
+ // Check for proper tense in Given
107
+ const givenLines = lines.filter((l) => l.startsWith("Given"));
108
+ for (const given of givenLines) {
109
+ if (/\bwill\b|\bshall\b/i.test(given)) {
110
+ result.warnings.push(
111
+ `Given step should be past tense, not future: "${given}". Reference: Given establishes existing state.`
112
+ );
113
+ }
114
+ }
115
+
116
+ // Check for single When action
117
+ const whenLines = lines.filter((l) => l.startsWith("When"));
118
+ if (whenLines.length > 1) {
119
+ const hasAnd = lines.some((l) => l.startsWith("And") &&
120
+ lines.indexOf(l) > lines.findIndex((l) => l.startsWith("When"))
121
+ );
122
+
123
+ if (!hasAnd) {
124
+ result.warnings.push(
125
+ "Multiple When steps should use 'And' for readability"
126
+ );
127
+ }
128
+ }
129
+
130
+ // Check for observable outcomes in Then
131
+ const thenLines = lines.filter((l) => l.startsWith("Then"));
132
+ for (const then of thenLines) {
133
+ const nonObservable = [
134
+ "database",
135
+ "queue",
136
+ "log",
137
+ "internal state",
138
+ "cache",
139
+ ];
140
+
141
+ for (const keyword of nonObservable) {
142
+ if (then.toLowerCase().includes(keyword)) {
143
+ result.warnings.push(
144
+ `Then step references internal state: "${then}". Then steps should verify user-observable outcomes only.`
145
+ );
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ private getScenarioStructure(scenario: string): string {
152
+ const lines = scenario.split("\n").map((l) => l.trim());
153
+ return lines
154
+ .filter((l) => l.startsWith("Given") || l.startsWith("When") || l.startsWith("Then"))
155
+ .map((l) => l.split(":")[0])
156
+ .join(" -> ");
157
+ }
158
+
159
+ private hasSimilarStructures(structures: string[]): boolean {
160
+ if (structures.length < 3) return false;
161
+
162
+ const counts = structures.reduce((acc, s) => {
163
+ acc[s] = (acc[s] || 0) + 1;
164
+ return acc;
165
+ }, {} as Record<string, number>);
166
+
167
+ return Object.values(counts).some((count) => count >= 3);
168
+ }
169
+
170
+ private formatValidationResults(result: ValidationResult): string {
171
+ let text = "# Gherkin Validation Results\n\n";
172
+
173
+ if (result.isValid && result.warnings.length === 0 && result.suggestions.length === 0) {
174
+ text += "✅ **Validation PASSED**\n\nYour feature file follows best practices!";
175
+ return text;
176
+ }
177
+
178
+ if (!result.isValid) {
179
+ text += "❌ **Validation FAILED**\n\n";
180
+ } else {
181
+ text += "⚠️ **Validation PASSED with warnings**\n\n";
182
+ }
183
+
184
+ if (result.errors.length > 0) {
185
+ text += "## Errors\n\n";
186
+ text += result.errors.map((e) => `- ❌ ${e}`).join("\n");
187
+ text += "\n\n";
188
+ }
189
+
190
+ if (result.warnings.length > 0) {
191
+ text += "## Warnings\n\n";
192
+ text += result.warnings.map((w) => `- ⚠️ ${w}`).join("\n");
193
+ text += "\n\n";
194
+ }
195
+
196
+ if (result.suggestions.length > 0) {
197
+ text += "## Suggestions\n\n";
198
+ text += result.suggestions.map((s) => `- 💡 ${s}`).join("\n");
199
+ text += "\n\n";
200
+ }
201
+
202
+ text += "\n**Reference:** bdd-specification/references/gherkin-patterns.md";
203
+
204
+ return text;
205
+ }
206
+ }
@@ -0,0 +1,42 @@
1
+ import { MethodologyRepository } from "../repository.js";
2
+
3
+ export class GuidanceSearcher {
4
+ constructor(private repository: MethodologyRepository) {}
5
+
6
+ async search(query: string) {
7
+ const results = await this.repository.searchContent(query);
8
+
9
+ if (results.length === 0) {
10
+ const skills = await this.repository.listSkills();
11
+ const skillList = skills.map((s) => s.name).join(", ");
12
+
13
+ return {
14
+ content: [
15
+ {
16
+ type: "text" as const,
17
+ text: `No results found for '${query}'.\n\nAvailable skills: ${skillList}`,
18
+ },
19
+ ],
20
+ };
21
+ }
22
+
23
+ // Take top 5 results
24
+ const topResults = results.slice(0, 5);
25
+
26
+ const text = topResults
27
+ .map(
28
+ (r, i) =>
29
+ `### ${i + 1}. ${r.file} (relevance: ${r.relevance})\n\n${r.content}\n`
30
+ )
31
+ .join("\n---\n\n");
32
+
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text" as const,
37
+ text: `# Search Results for '${query}'\n\n${text}`,
38
+ },
39
+ ],
40
+ };
41
+ }
42
+ }