project-graph-mcp 1.3.0 → 2.0.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.
Files changed (113) hide show
  1. package/README.md +223 -17
  2. package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +87 -30
  3. package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +23 -8
  4. package/package.json +12 -8
  5. package/src/.project-graph-cache.json +1 -0
  6. package/src/analysis/analysis-cache.js +7 -0
  7. package/src/analysis/complexity.js +14 -0
  8. package/src/analysis/custom-rules.js +36 -0
  9. package/src/analysis/db-analysis.js +9 -0
  10. package/src/analysis/dead-code.js +19 -0
  11. package/src/analysis/full-analysis.js +18 -0
  12. package/src/analysis/jsdoc-checker.js +24 -0
  13. package/src/analysis/jsdoc-generator.js +10 -0
  14. package/src/analysis/large-files.js +11 -0
  15. package/src/analysis/outdated-patterns.js +12 -0
  16. package/src/analysis/similar-functions.js +16 -0
  17. package/src/analysis/test-annotations.js +21 -0
  18. package/src/analysis/type-checker.js +8 -0
  19. package/src/analysis/undocumented.js +14 -0
  20. package/src/cli/cli-handlers.js +4 -0
  21. package/src/cli/cli.js +5 -0
  22. package/src/compact/ai-context.js +7 -0
  23. package/src/compact/compact.js +18 -0
  24. package/src/compact/compress.js +13 -0
  25. package/src/compact/ctx-to-jsdoc.js +29 -0
  26. package/src/compact/doc-dialect.js +30 -0
  27. package/src/compact/expand.js +37 -0
  28. package/src/compact/framework-references.js +5 -0
  29. package/src/compact/instructions.js +3 -0
  30. package/src/compact/mode-config.js +8 -0
  31. package/src/compact/validate-pipeline.js +9 -0
  32. package/src/core/event-bus.js +9 -0
  33. package/src/core/filters.js +14 -0
  34. package/src/core/graph-builder.js +12 -0
  35. package/src/core/parser.js +31 -0
  36. package/src/core/workspace.js +8 -0
  37. package/src/lang/lang-go.js +17 -0
  38. package/src/lang/lang-python.js +12 -0
  39. package/src/lang/lang-sql.js +23 -0
  40. package/src/lang/lang-typescript.js +9 -0
  41. package/src/lang/lang-utils.js +4 -0
  42. package/src/mcp/mcp-server.js +17 -0
  43. package/src/mcp/tool-defs.js +3 -0
  44. package/src/mcp/tools.js +25 -0
  45. package/src/network/backend-lifecycle.js +19 -0
  46. package/src/network/backend.js +5 -0
  47. package/src/network/local-gateway.js +23 -0
  48. package/src/network/mdns.js +13 -0
  49. package/src/network/server.js +10 -0
  50. package/src/network/web-server.js +34 -0
  51. package/vendor/terser.mjs +49 -0
  52. package/web/.project-graph-cache.json +1 -0
  53. package/web/app.js +16 -0
  54. package/web/components/code-block.js +3 -0
  55. package/web/components/quick-open.js +5 -0
  56. package/web/dashboard-state.js +3 -0
  57. package/web/dashboard.html +27 -0
  58. package/web/dashboard.js +8 -0
  59. package/web/highlight.js +13 -0
  60. package/web/index.html +35 -0
  61. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  62. package/web/panels/ActionBoard/ActionBoard.js +4 -0
  63. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  64. package/web/panels/EventItem/EventItem.css.js +1 -0
  65. package/web/panels/EventItem/EventItem.js +4 -0
  66. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  67. package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
  68. package/web/panels/ProjectItem/ProjectItem.js +5 -0
  69. package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
  70. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  71. package/web/panels/ProjectList/ProjectList.js +4 -0
  72. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  73. package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
  74. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  75. package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
  76. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  77. package/web/panels/code-viewer.js +5 -0
  78. package/web/panels/ctx-panel.js +4 -0
  79. package/web/panels/dep-graph.js +6 -0
  80. package/web/panels/file-tree.js +188 -0
  81. package/web/panels/health-panel.js +3 -0
  82. package/web/panels/live-monitor.js +3 -0
  83. package/web/state.js +17 -0
  84. package/web/style.css +157 -0
  85. package/references/symbiote-3x.md +0 -834
  86. package/src/cli-handlers.js +0 -140
  87. package/src/cli.js +0 -83
  88. package/src/complexity.js +0 -223
  89. package/src/custom-rules.js +0 -583
  90. package/src/db-analysis.js +0 -194
  91. package/src/dead-code.js +0 -468
  92. package/src/filters.js +0 -227
  93. package/src/framework-references.js +0 -177
  94. package/src/full-analysis.js +0 -174
  95. package/src/graph-builder.js +0 -299
  96. package/src/instructions.js +0 -175
  97. package/src/jsdoc-generator.js +0 -214
  98. package/src/lang-go.js +0 -285
  99. package/src/lang-python.js +0 -197
  100. package/src/lang-sql.js +0 -309
  101. package/src/lang-typescript.js +0 -190
  102. package/src/lang-utils.js +0 -124
  103. package/src/large-files.js +0 -162
  104. package/src/mcp-server.js +0 -468
  105. package/src/outdated-patterns.js +0 -295
  106. package/src/parser.js +0 -452
  107. package/src/server.js +0 -28
  108. package/src/similar-functions.js +0 -278
  109. package/src/test-annotations.js +0 -301
  110. package/src/tool-defs.js +0 -525
  111. package/src/tools.js +0 -470
  112. package/src/undocumented.js +0 -260
  113. package/src/workspace.js +0 -70
@@ -1,278 +0,0 @@
1
- /**
2
- * Similar Functions Detector
3
- * Finds functionally similar functions across the codebase
4
- */
5
-
6
- import { readFileSync, readdirSync, statSync } from 'fs';
7
- import { join, relative, resolve } from 'path';
8
- import { parse } from '../vendor/acorn.mjs';
9
- import * as walk from '../vendor/walk.mjs';
10
- import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.js';
11
-
12
- /**
13
- * @typedef {Object} FunctionSignature
14
- * @property {string} name
15
- * @property {string} file
16
- * @property {number} line
17
- * @property {number} paramCount
18
- * @property {string[]} paramNames
19
- * @property {boolean} async
20
- * @property {string} bodyHash - Structural hash of function body
21
- * @property {string[]} calls - Functions called inside
22
- */
23
-
24
- /**
25
- * @typedef {Object} SimilarPair
26
- * @property {FunctionSignature} a
27
- * @property {FunctionSignature} b
28
- * @property {number} similarity - 0-100
29
- * @property {string[]} reasons
30
- */
31
-
32
- /**
33
- * Find all JS files
34
- * @param {string} dir
35
- * @param {string} rootDir
36
- * @returns {string[]}
37
- */
38
- function findJSFiles(dir, rootDir = dir) {
39
- if (dir === rootDir) parseGitignore(rootDir);
40
- const files = [];
41
-
42
- try {
43
- for (const entry of readdirSync(dir)) {
44
- const fullPath = join(dir, entry);
45
- const relativePath = relative(rootDir, fullPath);
46
- const stat = statSync(fullPath);
47
-
48
- if (stat.isDirectory()) {
49
- if (!shouldExcludeDir(entry, relativePath)) {
50
- files.push(...findJSFiles(fullPath, rootDir));
51
- }
52
- } else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
53
- if (!shouldExcludeFile(entry, relativePath)) {
54
- files.push(fullPath);
55
- }
56
- }
57
- }
58
- } catch (e) { }
59
-
60
- return files;
61
- }
62
-
63
- /**
64
- * Extract function signatures from a file
65
- * @param {string} filePath
66
- * @returns {FunctionSignature[]}
67
- */
68
- function extractSignatures(filePath, rootDir) {
69
- const code = readFileSync(filePath, 'utf-8');
70
- const relPath = relative(rootDir, filePath);
71
- const signatures = [];
72
-
73
- let ast;
74
- try {
75
- ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
76
- } catch (e) {
77
- return signatures;
78
- }
79
-
80
- walk.simple(ast, {
81
- FunctionDeclaration(node) {
82
- if (!node.id) return;
83
- signatures.push(buildSignature(node, node.id.name, relPath));
84
- },
85
-
86
- MethodDefinition(node) {
87
- if (node.kind !== 'method') return;
88
- const name = node.key.name || node.key.value;
89
- if (name.startsWith('_')) return;
90
- signatures.push(buildSignature(node.value, name, relPath));
91
- },
92
- });
93
-
94
- return signatures;
95
- }
96
-
97
- /**
98
- * Build signature from function node
99
- * @param {Object} node
100
- * @param {string} name
101
- * @param {string} file
102
- * @returns {FunctionSignature}
103
- */
104
- function buildSignature(node, name, file) {
105
- const paramNames = node.params.map(p => extractParamName(p));
106
- const calls = [];
107
-
108
- // Extract function calls
109
- walk.simple(node.body, {
110
- CallExpression(callNode) {
111
- if (callNode.callee.type === 'Identifier') {
112
- calls.push(callNode.callee.name);
113
- } else if (callNode.callee.type === 'MemberExpression' && callNode.callee.property.type === 'Identifier') {
114
- calls.push(callNode.callee.property.name);
115
- }
116
- },
117
- });
118
-
119
- // Create structural hash
120
- const bodyHash = hashBodyStructure(node.body);
121
-
122
- return {
123
- name,
124
- file,
125
- line: node.loc?.start?.line || 0,
126
- paramCount: node.params.length,
127
- paramNames,
128
- async: node.async || false,
129
- bodyHash,
130
- calls: [...new Set(calls)],
131
- };
132
- }
133
-
134
- /**
135
- * Extract param name
136
- * @param {Object} param
137
- * @returns {string}
138
- */
139
- function extractParamName(param) {
140
- if (param.type === 'Identifier') return param.name;
141
- if (param.type === 'AssignmentPattern' && param.left.type === 'Identifier') return param.left.name;
142
- if (param.type === 'RestElement' && param.argument.type === 'Identifier') return param.argument.name;
143
- return 'param';
144
- }
145
-
146
- /**
147
- * Create structural hash of function body
148
- * @param {Object} body
149
- * @returns {string}
150
- */
151
- function hashBodyStructure(body) {
152
- const structure = [];
153
-
154
- walk.simple(body, {
155
- IfStatement() { structure.push('IF'); },
156
- ForStatement() { structure.push('FOR'); },
157
- ForOfStatement() { structure.push('FOROF'); },
158
- ForInStatement() { structure.push('FORIN'); },
159
- WhileStatement() { structure.push('WHILE'); },
160
- SwitchStatement() { structure.push('SWITCH'); },
161
- TryStatement() { structure.push('TRY'); },
162
- ReturnStatement() { structure.push('RET'); },
163
- ThrowStatement() { structure.push('THROW'); },
164
- AwaitExpression() { structure.push('AWAIT'); },
165
- });
166
-
167
- return structure.join('|');
168
- }
169
-
170
- /**
171
- * Calculate similarity between two functions
172
- * @param {FunctionSignature} a
173
- * @param {FunctionSignature} b
174
- * @returns {{similarity: number, reasons: string[]}}
175
- */
176
- function calculateSimilarity(a, b) {
177
- const reasons = [];
178
- let score = 0;
179
- let maxScore = 0;
180
-
181
- // Same param count (important)
182
- maxScore += 30;
183
- if (a.paramCount === b.paramCount) {
184
- score += 30;
185
- reasons.push('Same param count');
186
- }
187
-
188
- // Similar param names
189
- maxScore += 20;
190
- const commonParams = a.paramNames.filter(p => b.paramNames.includes(p));
191
- if (commonParams.length > 0 && a.paramNames.length > 0) {
192
- const paramSim = commonParams.length / Math.max(a.paramNames.length, b.paramNames.length);
193
- score += Math.round(paramSim * 20);
194
- if (paramSim >= 0.5) reasons.push(`Similar params: ${commonParams.join(', ')}`);
195
- }
196
-
197
- // Same async status
198
- maxScore += 10;
199
- if (a.async === b.async) {
200
- score += 10;
201
- }
202
-
203
- // Similar body structure
204
- maxScore += 25;
205
- if (a.bodyHash === b.bodyHash && a.bodyHash.length > 0) {
206
- score += 25;
207
- reasons.push('Identical structure');
208
- } else if (a.bodyHash.length > 0 && b.bodyHash.length > 0) {
209
- const aTokens = a.bodyHash.split('|');
210
- const bTokens = b.bodyHash.split('|');
211
- const commonTokens = aTokens.filter(t => bTokens.includes(t));
212
- if (commonTokens.length > 0) {
213
- const structSim = commonTokens.length / Math.max(aTokens.length, bTokens.length);
214
- score += Math.round(structSim * 25);
215
- if (structSim >= 0.5) reasons.push('Similar control flow');
216
- }
217
- }
218
-
219
- // Common function calls
220
- maxScore += 15;
221
- const commonCalls = a.calls.filter(c => b.calls.includes(c));
222
- if (commonCalls.length > 0 && a.calls.length > 0 && b.calls.length > 0) {
223
- const callSim = commonCalls.length / Math.max(a.calls.length, b.calls.length);
224
- score += Math.round(callSim * 15);
225
- if (commonCalls.length >= 2) reasons.push(`Common calls: ${commonCalls.slice(0, 3).join(', ')}`);
226
- }
227
-
228
- const similarity = Math.round((score / maxScore) * 100);
229
- return { similarity, reasons };
230
- }
231
-
232
- /**
233
- * Get similar functions in directory
234
- * @param {string} dir
235
- * @param {Object} [options]
236
- * @param {number} [options.threshold=60] - Minimum similarity percentage
237
- * @returns {Promise<{total: number, pairs: SimilarPair[]}>}
238
- */
239
- export async function getSimilarFunctions(dir, options = {}) {
240
- const threshold = options.threshold || 60;
241
- const resolvedDir = resolve(dir);
242
- const files = findJSFiles(dir);
243
- const allSignatures = [];
244
-
245
- // Collect all signatures
246
- for (const file of files) {
247
- allSignatures.push(...extractSignatures(file, resolvedDir));
248
- }
249
-
250
- // Compare all pairs
251
- const pairs = [];
252
- for (let i = 0; i < allSignatures.length; i++) {
253
- for (let j = i + 1; j < allSignatures.length; j++) {
254
- const a = allSignatures[i];
255
- const b = allSignatures[j];
256
-
257
- // Skip same file same name (likely intentional overload)
258
- if (a.file === b.file && a.name === b.name) continue;
259
-
260
- // Skip very small functions
261
- if (a.bodyHash.length < 3 && b.bodyHash.length < 3) continue;
262
-
263
- const { similarity, reasons } = calculateSimilarity(a, b);
264
-
265
- if (similarity >= threshold && reasons.length > 0) {
266
- pairs.push({ a, b, similarity, reasons });
267
- }
268
- }
269
- }
270
-
271
- // Sort by similarity descending
272
- pairs.sort((x, y) => y.similarity - x.similarity);
273
-
274
- return {
275
- total: pairs.length,
276
- pairs: pairs.slice(0, 20),
277
- };
278
- }
@@ -1,301 +0,0 @@
1
- /**
2
- * Test Annotations Parser
3
- * Extracts @test/@expect JSDoc annotations for browser testing
4
- */
5
-
6
- import { readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
7
- import { join, basename, relative, resolve } from 'path';
8
-
9
- /**
10
- * @typedef {Object} TestStep
11
- * @property {string} id - Unique ID (e.g., "togglePin.1")
12
- * @property {string} type - Action type (click, key, drag, etc.)
13
- * @property {string} description - What to do
14
- * @property {boolean} completed - Whether test passed
15
- * @property {string} [failReason] - Why it failed (if failed)
16
- */
17
-
18
- /**
19
- * @typedef {Object} Feature
20
- * @property {string} name - Method name
21
- * @property {string} description - What the method does
22
- * @property {TestStep[]} tests - Test steps
23
- * @property {Array<{type: string, description: string}>} expects - Expected outcomes
24
- * @property {string} file - Source file
25
- * @property {number} line - Line number
26
- */
27
-
28
- // In-memory state for test progress
29
- const testState = new Map();
30
-
31
- /**
32
- * Parse @test/@expect annotations from a file
33
- * @param {string} content
34
- * @param {string} filePath
35
- * @returns {Feature[]}
36
- */
37
- export function parseAnnotations(content, filePath) {
38
- const results = [];
39
- const blockRegex = /\/\*\*([^]*?)\*\//g;
40
-
41
- let match;
42
- while ((match = blockRegex.exec(content)) !== null) {
43
- const block = match[1];
44
-
45
- // Check if block has @test or @expect
46
- if (!block.includes('@test') && !block.includes('@expect')) continue;
47
-
48
- // Find method name after the block
49
- // Supports: methodName( | propName: ( | propName: async (
50
- const afterBlock = content.slice(match.index + match[0].length);
51
- const methodMatch = afterBlock.match(
52
- /^\s*(?:async\s+)?(\w+)\s*\(/ // class method: methodName(
53
- ) || afterBlock.match(
54
- /^\s*(\w+)\s*:\s*(?:async\s*)?\(/ // arrow in object: propName: (
55
- ) || afterBlock.match(
56
- /^\s*(\w+)\s*:\s*(?:async\s+)?\(/ // arrow in object: propName: async (
57
- );
58
- if (!methodMatch) continue;
59
-
60
- const methodName = methodMatch[1];
61
-
62
- // Extract description (first line)
63
- const descMatch = block.match(/^\s*\*\s*([^@\n][^\n]*)/m);
64
- const description = descMatch ? descMatch[1].trim() : methodName;
65
-
66
- // Extract @test annotations with unique IDs
67
- const tests = [];
68
- const testRegex = /@test\s+(\w+):\s*(.+)/g;
69
- let testMatch;
70
- let testIndex = 0;
71
- while ((testMatch = testRegex.exec(block)) !== null) {
72
- tests.push({
73
- id: `${methodName}.${testIndex++}`,
74
- type: testMatch[1],
75
- description: testMatch[2].trim(),
76
- completed: false,
77
- failReason: null,
78
- });
79
- }
80
-
81
- // Extract @expect annotations
82
- const expects = [];
83
- const expectRegex = /@expect\s+(\w+):\s*(.+)/g;
84
- let expectMatch;
85
- while ((expectMatch = expectRegex.exec(block)) !== null) {
86
- expects.push({
87
- type: expectMatch[1],
88
- description: expectMatch[2].trim(),
89
- });
90
- }
91
-
92
- if (tests.length || expects.length) {
93
- const lineNumber = content.slice(0, match.index).split('\n').length;
94
-
95
- results.push({
96
- name: methodName,
97
- description,
98
- tests,
99
- expects,
100
- file: filePath,
101
- line: lineNumber,
102
- });
103
- }
104
- }
105
-
106
- return results;
107
- }
108
-
109
- /**
110
- * Find all JS files in directory
111
- * @param {string} dir
112
- * @returns {string[]}
113
- */
114
- function findJSFiles(dir) {
115
- const files = [];
116
-
117
- try {
118
- for (const entry of readdirSync(dir)) {
119
- const fullPath = join(dir, entry);
120
- const stat = statSync(fullPath);
121
-
122
- if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
123
- files.push(...findJSFiles(fullPath));
124
- } else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
125
- files.push(fullPath);
126
- }
127
- }
128
- } catch (e) {
129
- // Directory not found
130
- }
131
-
132
- return files;
133
- }
134
-
135
- /**
136
- * Get all features from a directory
137
- * @param {string} dir
138
- * @returns {Feature[]}
139
- */
140
- export function getAllFeatures(dir) {
141
- const files = findJSFiles(dir);
142
- const features = [];
143
-
144
- for (const file of files) {
145
- const content = readFileSync(file, 'utf-8');
146
- const parsed = parseAnnotations(content, file);
147
- features.push(...parsed);
148
- }
149
-
150
- return features;
151
- }
152
-
153
- /**
154
- * Get pending (uncompleted) tests
155
- * @param {string} dir
156
- * @returns {TestStep[]}
157
- */
158
- export function getPendingTests(dir) {
159
- const resolvedDir = resolve(dir);
160
- const features = getAllFeatures(dir);
161
- const pending = [];
162
-
163
- for (const feature of features) {
164
- for (const test of feature.tests) {
165
- const state = testState.get(test.id);
166
- if (!state || !state.completed) {
167
- pending.push({
168
- ...test,
169
- feature: feature.name,
170
- file: relative(resolvedDir, feature.file),
171
- });
172
- }
173
- }
174
- }
175
-
176
- return pending;
177
- }
178
-
179
- /**
180
- * Mark a test as passed
181
- * @param {string} testId
182
- */
183
- export function markTestPassed(testId) {
184
- testState.set(testId, { completed: true, passed: true });
185
- return { success: true, testId };
186
- }
187
-
188
- /**
189
- * Mark a test as failed
190
- * @param {string} testId
191
- * @param {string} reason
192
- */
193
- export function markTestFailed(testId, reason) {
194
- testState.set(testId, { completed: true, passed: false, reason });
195
- return { success: true, testId, reason };
196
- }
197
-
198
- /**
199
- * Get test summary
200
- * @param {string} dir
201
- * @returns {Object}
202
- */
203
- export function getTestSummary(dir) {
204
- const features = getAllFeatures(dir);
205
-
206
- let total = 0;
207
- let passed = 0;
208
- let failed = 0;
209
- let pending = 0;
210
- const failures = [];
211
-
212
- for (const feature of features) {
213
- for (const test of feature.tests) {
214
- total++;
215
- const state = testState.get(test.id);
216
-
217
- if (!state || !state.completed) {
218
- pending++;
219
- } else if (state.passed) {
220
- passed++;
221
- } else {
222
- failed++;
223
- failures.push({
224
- id: test.id,
225
- reason: state.reason,
226
- });
227
- }
228
- }
229
- }
230
-
231
- return {
232
- total,
233
- passed,
234
- failed,
235
- pending,
236
- progress: total > 0 ? Math.round((passed + failed) / total * 100) : 0,
237
- failures,
238
- };
239
- }
240
-
241
- /**
242
- * Reset test state
243
- */
244
- export function resetTestState() {
245
- testState.clear();
246
- return { success: true };
247
- }
248
-
249
- /**
250
- * Generate markdown checklist
251
- * @param {Feature[]} features
252
- * @returns {string}
253
- */
254
- export function generateMarkdown(features) {
255
- const lines = [
256
- '# Browser Test Checklist',
257
- '',
258
- `> Auto-generated from JSDoc @test/@expect annotations`,
259
- `> Generated: ${new Date().toISOString().split('T')[0]}`,
260
- '',
261
- ];
262
-
263
- // Group by file
264
- const byFile = {};
265
- for (const feature of features) {
266
- const key = feature.file;
267
- if (!byFile[key]) byFile[key] = [];
268
- byFile[key].push(feature);
269
- }
270
-
271
- for (const [file, fileFeatures] of Object.entries(byFile)) {
272
- lines.push(`## ${basename(file, '.js')}`);
273
- lines.push('');
274
-
275
- for (const feature of fileFeatures) {
276
- lines.push(`### ${feature.name}()`);
277
- lines.push(`${feature.description}`);
278
- lines.push('');
279
-
280
- if (feature.tests.length) {
281
- lines.push('**Steps:**');
282
- for (const test of feature.tests) {
283
- const state = testState.get(test.id);
284
- const check = state?.passed ? '[x]' : '[ ]';
285
- lines.push(`- ${check} \`${test.type}\`: ${test.description}`);
286
- }
287
- lines.push('');
288
- }
289
-
290
- if (feature.expects.length) {
291
- lines.push('**Expected:**');
292
- for (const expect of feature.expects) {
293
- lines.push(`- ✅ \`${expect.type}\`: ${expect.description}`);
294
- }
295
- lines.push('');
296
- }
297
- }
298
- }
299
-
300
- return lines.join('\n');
301
- }