forge-workflow 0.0.3 → 0.0.5

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 (209) hide show
  1. package/.claude/commands/dev.md +340 -314
  2. package/.claude/commands/plan.md +521 -478
  3. package/.claude/commands/premerge.md +176 -179
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +442 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +164 -134
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +48 -77
  10. package/.claude/commands/validate.md +282 -237
  11. package/.claude/commands/verify.md +221 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +105 -105
  14. package/.claude/scripts/greptile-resolve.sh +526 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +337 -311
  17. package/.cline/workflows/plan.md +518 -475
  18. package/.cline/workflows/premerge.md +173 -176
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +439 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +161 -131
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +45 -74
  25. package/.cline/workflows/validate.md +279 -234
  26. package/.cline/workflows/verify.md +218 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +340 -314
  29. package/.codex/skills/plan/SKILL.md +521 -478
  30. package/.codex/skills/premerge/SKILL.md +176 -179
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +442 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +164 -134
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +48 -77
  37. package/.codex/skills/validate/SKILL.md +282 -237
  38. package/.codex/skills/verify/SKILL.md +221 -221
  39. package/.cursor/commands/dev.md +337 -311
  40. package/.cursor/commands/plan.md +518 -475
  41. package/.cursor/commands/premerge.md +173 -176
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +439 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +161 -131
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +45 -74
  48. package/.cursor/commands/validate.md +279 -234
  49. package/.cursor/commands/verify.md +218 -218
  50. package/.cursor/rules/permissions-guidance.mdc +37 -37
  51. package/.forge/hooks/check-tdd.js +240 -240
  52. package/.github/PLUGIN_TEMPLATE.json +32 -32
  53. package/.github/prompts/dev.prompt.md +342 -316
  54. package/.github/prompts/plan.prompt.md +523 -480
  55. package/.github/prompts/premerge.prompt.md +178 -181
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +444 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +166 -136
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +50 -79
  62. package/.github/prompts/validate.prompt.md +284 -239
  63. package/.github/prompts/verify.prompt.md +223 -223
  64. package/.github/workflows/beads-to-github.yml +56 -0
  65. package/.github/workflows/github-to-beads.yml +97 -0
  66. package/.kilocode/workflows/dev.md +341 -315
  67. package/.kilocode/workflows/plan.md +522 -479
  68. package/.kilocode/workflows/premerge.md +177 -180
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +443 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +165 -135
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +49 -78
  75. package/.kilocode/workflows/validate.md +283 -238
  76. package/.kilocode/workflows/verify.md +222 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +340 -314
  79. package/.opencode/commands/plan.md +521 -478
  80. package/.opencode/commands/premerge.md +176 -179
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +442 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +164 -134
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +48 -77
  87. package/.opencode/commands/validate.md +282 -237
  88. package/.opencode/commands/verify.md +221 -221
  89. package/.roo/commands/dev.md +341 -315
  90. package/.roo/commands/plan.md +522 -479
  91. package/.roo/commands/premerge.md +177 -180
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +443 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +165 -135
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +49 -78
  98. package/.roo/commands/validate.md +283 -238
  99. package/.roo/commands/verify.md +222 -222
  100. package/AGENTS.md +175 -169
  101. package/CLAUDE.md +100 -99
  102. package/LICENSE +21 -21
  103. package/README.md +429 -414
  104. package/bin/forge-cmd.js +313 -313
  105. package/bin/{forge-validate.js → forge-preflight.js} +309 -303
  106. package/bin/forge.js +4596 -4232
  107. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  108. package/docs/BEADS_GITHUB_SYNC.md +251 -0
  109. package/docs/ENHANCED_ONBOARDING.md +602 -602
  110. package/docs/EXAMPLES.md +482 -482
  111. package/docs/GREPTILE_SETUP.md +400 -400
  112. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  113. package/docs/ROADMAP.md +359 -359
  114. package/docs/SETUP.md +663 -632
  115. package/docs/TOOLCHAIN.md +630 -630
  116. package/docs/VALIDATION.md +363 -363
  117. package/install.sh +40 -1058
  118. package/lefthook.yml +39 -39
  119. package/lib/agents/README.md +198 -198
  120. package/lib/agents/claude.plugin.json +28 -28
  121. package/lib/agents/cline.plugin.json +22 -22
  122. package/lib/agents/codex.plugin.json +19 -19
  123. package/lib/agents/copilot.plugin.json +24 -24
  124. package/lib/agents/cursor.plugin.json +25 -25
  125. package/lib/agents/kilocode.plugin.json +22 -22
  126. package/lib/agents/opencode.plugin.json +20 -20
  127. package/lib/agents/roo.plugin.json +23 -23
  128. package/lib/agents-config.js +2112 -2112
  129. package/lib/beads-health-check.js +143 -0
  130. package/lib/beads-setup.js +341 -0
  131. package/lib/beads-sync-scaffold.js +260 -0
  132. package/lib/commands/dev.js +513 -513
  133. package/lib/commands/plan.js +692 -692
  134. package/lib/commands/recommend.js +119 -119
  135. package/lib/commands/ship.js +377 -377
  136. package/lib/commands/status.js +378 -378
  137. package/lib/commands/validate.js +602 -602
  138. package/lib/context-merge.js +359 -359
  139. package/lib/dep-guard/analyzer.js +294 -294
  140. package/lib/dep-guard/behavior-detector.js +98 -98
  141. package/lib/dep-guard/contract-detector.js +162 -162
  142. package/lib/dep-guard/import-detector.js +498 -498
  143. package/lib/dep-guard/path-utils.js +13 -13
  144. package/lib/dep-guard/rubric.js +120 -120
  145. package/lib/dep-guard/task-parser.js +318 -318
  146. package/lib/detect-agent.js +191 -0
  147. package/lib/detect-worktree.js +47 -0
  148. package/lib/file-hash.js +26 -0
  149. package/lib/husky-migration.js +450 -0
  150. package/lib/lefthook-check.js +65 -0
  151. package/lib/pat-setup.js +207 -0
  152. package/lib/plugin-catalog.js +350 -350
  153. package/lib/plugin-manager.js +166 -166
  154. package/lib/plugin-recommender.js +141 -141
  155. package/lib/project-discovery.js +491 -491
  156. package/lib/setup-action-log.js +139 -0
  157. package/lib/setup-summary-renderer.js +106 -0
  158. package/lib/setup-utils.js +96 -0
  159. package/lib/setup.js +192 -118
  160. package/lib/smart-merge.js +64 -0
  161. package/lib/symlink-utils.js +81 -0
  162. package/lib/workflow-profiles.js +197 -197
  163. package/package.json +131 -129
  164. package/scripts/beads-context.sh +291 -0
  165. package/scripts/beads-context.test.js +563 -0
  166. package/scripts/behavioral-judge.sh +378 -0
  167. package/scripts/benchmark.js +85 -0
  168. package/scripts/branch-protection.js +183 -0
  169. package/scripts/check-agents.js +172 -0
  170. package/scripts/commitlint.js +42 -0
  171. package/scripts/conflict-detect.sh +323 -0
  172. package/scripts/dep-guard-analyze.js +71 -0
  173. package/scripts/dep-guard.sh +811 -0
  174. package/scripts/eval_win.py +249 -0
  175. package/scripts/file-index.sh +399 -0
  176. package/scripts/github-beads-sync/comment.mjs +64 -0
  177. package/scripts/github-beads-sync/config.mjs +148 -0
  178. package/scripts/github-beads-sync/github-api.mjs +131 -0
  179. package/scripts/github-beads-sync/index.mjs +332 -0
  180. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  181. package/scripts/github-beads-sync/mapping.mjs +78 -0
  182. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  183. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  184. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  185. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  186. package/scripts/github-beads-sync.config.json +26 -0
  187. package/scripts/improve-command.js +375 -0
  188. package/scripts/lib/eval-runner.js +229 -0
  189. package/scripts/lib/eval-schema.js +135 -0
  190. package/scripts/lib/eval-storage.js +78 -0
  191. package/scripts/lib/grading.js +203 -0
  192. package/scripts/lib/transcript-parser.js +63 -0
  193. package/scripts/lint.js +47 -0
  194. package/scripts/migrate-to-bun-test.js +412 -0
  195. package/scripts/run-command-eval.js +236 -0
  196. package/scripts/smart-status.sh +782 -0
  197. package/scripts/sync-commands.js +571 -0
  198. package/scripts/sync-utils.sh +460 -0
  199. package/scripts/test-dashboard.js +123 -0
  200. package/scripts/test.js +44 -0
  201. package/scripts/validate.sh +94 -0
  202. package/skills/parallel-deep-research/SKILL.md +108 -108
  203. package/skills/parallel-deep-research/evals/README.md +27 -27
  204. package/skills/parallel-deep-research/evals/evals.json +62 -62
  205. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  206. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  207. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  208. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  209. package/docs/WORKFLOW.md +0 -400
@@ -1,359 +1,359 @@
1
- /**
2
- * Semantic Merge for Context Files
3
- *
4
- * Intelligently merges existing CLAUDE.md/AGENTS.md files with Forge workflow templates
5
- * by understanding the semantic meaning of markdown sections.
6
- */
7
-
8
- const { distance: levenshteinDistance } = require('fastest-levenshtein');
9
-
10
- // Section category definitions
11
- const SECTION_CATEGORIES = {
12
- preserve: [
13
- 'Project Description',
14
- 'Project Instructions',
15
- 'Project Overview',
16
- 'Project Background',
17
- 'Domain Knowledge',
18
- 'Domain Concepts',
19
- 'Coding Standards',
20
- 'Code Standards',
21
- 'Architecture',
22
- 'Tech Stack',
23
- 'Technology Stack',
24
- 'Build Commands',
25
- 'Team Conventions',
26
- 'Migration Strategy',
27
- 'Setup',
28
- 'Installation',
29
- 'Quick Start',
30
- 'Getting Started'
31
- ],
32
-
33
- replace: [
34
- 'Workflow',
35
- 'Development Workflow',
36
- 'Our Workflow',
37
- 'Workflow Process',
38
- 'Development Process',
39
- 'Process',
40
- 'TDD',
41
- 'Test-Driven Development',
42
- 'TDD Approach',
43
- 'Testing Approach',
44
- 'Git Workflow',
45
- 'Git Conventions',
46
- 'Commit Conventions',
47
- 'Git Strategy',
48
- 'Forge Workflow',
49
- 'Core Principles',
50
- 'Development Principles'
51
- ],
52
-
53
- merge: [
54
- 'Toolchain',
55
- 'Tools',
56
- 'MCP Servers',
57
- 'Integrations',
58
- 'Dependencies',
59
- 'Libraries'
60
- ]
61
- };
62
-
63
- /**
64
- * Parse markdown content into semantic sections
65
- * @param {string} markdownContent - Raw markdown content
66
- * @returns {Array} Array of section objects with structure:
67
- * { level, header, content, raw, startLine, endLine }
68
- */
69
- function parseSemanticSections(markdownContent) {
70
- if (!markdownContent || typeof markdownContent !== 'string') {
71
- return [];
72
- }
73
-
74
- const lines = markdownContent.split('\n');
75
- const sections = [];
76
- let currentSection = null;
77
-
78
- for (let i = 0; i < lines.length; i++) {
79
- const line = lines[i];
80
- // Use RegExp.exec() instead of String.match() per S5852 recommendation
81
- const headerMatch = /^(#{1,6})\s+([^\r\n]+)$/.exec(line); // NOSONAR S5852 - uses [^\r\n]+ (bounded, no backtracking), developer tool context
82
-
83
- if (headerMatch) {
84
- // Save previous section if exists
85
- if (currentSection) {
86
- currentSection.endLine = i - 1;
87
- currentSection.content = currentSection.content.trim();
88
- sections.push(currentSection);
89
- }
90
-
91
- // Start new section
92
- currentSection = {
93
- level: headerMatch[1].length,
94
- header: headerMatch[2].trim(),
95
- content: '',
96
- raw: line,
97
- startLine: i
98
- };
99
- } else if (currentSection) {
100
- // Add content to current section
101
- currentSection.content += line + '\n';
102
- currentSection.raw += '\n' + line;
103
- } else if (line.trim() !== '') {
104
- // Content before first header (preamble)
105
- sections.push({
106
- level: 0,
107
- header: null,
108
- content: line,
109
- raw: line,
110
- startLine: i,
111
- endLine: i
112
- });
113
- }
114
- }
115
-
116
- // Save last section
117
- if (currentSection) {
118
- currentSection.endLine = lines.length - 1;
119
- currentSection.content = currentSection.content.trim();
120
- sections.push(currentSection);
121
- }
122
-
123
- return sections;
124
- }
125
-
126
- /**
127
- * Calculate similarity between normalized text and keyword
128
- * Reduces cognitive complexity by extracting matching logic (S3776)
129
- * @param {string} normalized - Normalized header text
130
- * @param {string} keywordNorm - Normalized keyword
131
- * @returns {number} - Similarity score 0-1
132
- */
133
- function calculateKeywordSimilarity(normalized, keywordNorm) {
134
- // Fuzzy match using Levenshtein distance
135
- const distance = levenshteinDistance(normalized, keywordNorm);
136
- const maxLen = Math.max(normalized.length, keywordNorm.length);
137
- const similarity = 1 - (distance / maxLen);
138
-
139
- // Also check if normalized contains the keyword
140
- if (normalized.includes(keywordNorm) || keywordNorm.includes(normalized)) {
141
- const containsSimilarity = Math.min(normalized.length, keywordNorm.length) / maxLen;
142
- return Math.max(similarity, containsSimilarity);
143
- }
144
-
145
- return similarity;
146
- }
147
-
148
- /**
149
- * Detect the category of a section based on its header
150
- * @param {string} headerText - Section header text
151
- * @returns {Object} { category: 'preserve'|'replace'|'merge'|'unknown', confidence: 0-1 }
152
- */
153
- function detectCategory(headerText) {
154
- if (!headerText || typeof headerText !== 'string') {
155
- return { category: 'unknown', confidence: 0 };
156
- }
157
-
158
- const normalized = headerText.toLowerCase().trim();
159
- let bestMatch = { category: 'unknown', confidence: 0 };
160
-
161
- // Check each category
162
- for (const [category, keywords] of Object.entries(SECTION_CATEGORIES)) {
163
- for (const keyword of keywords) {
164
- const keywordNorm = keyword.toLowerCase().trim();
165
-
166
- // Exact match = highest confidence
167
- if (normalized === keywordNorm) {
168
- return { category, confidence: 1 };
169
- }
170
-
171
- // Calculate similarity and update best match
172
- const similarity = calculateKeywordSimilarity(normalized, keywordNorm);
173
- if (similarity > bestMatch.confidence) {
174
- bestMatch = { category, confidence: similarity };
175
- }
176
- }
177
- }
178
-
179
- return bestMatch;
180
- }
181
-
182
- /**
183
- * Build merged document from categorized sections
184
- * @param {Array} existingSections - Sections from existing file
185
- * @param {Array} forgeSections - Sections from Forge template
186
- * @param {Object} options - Merge options
187
- * @returns {string} Merged markdown content
188
- */
189
- function buildMergedDocument(existingSections, forgeSections, _options = {}) {
190
- const result = [];
191
- const processedExisting = new Set();
192
-
193
- // Process forge sections first to establish structure
194
- forgeSections.forEach(forgeSection => {
195
- if (!forgeSection.header) {
196
- return; // Skip preamble from forge
197
- }
198
-
199
- const forgeCategory = detectCategory(forgeSection.header);
200
-
201
- if (forgeCategory.category === 'replace' && forgeCategory.confidence > 0.6) {
202
- // This is a workflow/TDD section - use forge version
203
- result.push(forgeSection.raw);
204
-
205
- // Mark any similar existing sections as processed
206
- existingSections.forEach((existingSection, idx) => {
207
- if (existingSection.header) {
208
- const existingCategory = detectCategory(existingSection.header);
209
- if (existingCategory.category === 'replace' && existingCategory.confidence > 0.6) {
210
- // Check if headers are similar enough
211
- const normalized1 = forgeSection.header.toLowerCase();
212
- const normalized2 = existingSection.header.toLowerCase();
213
- const distance = levenshteinDistance(normalized1, normalized2);
214
- const similarity = 1 - (distance / Math.max(normalized1.length, normalized2.length));
215
-
216
- if (similarity > 0.5) {
217
- processedExisting.add(idx);
218
- }
219
- }
220
- }
221
- });
222
- } else if (forgeCategory.category === 'merge' && forgeCategory.confidence > 0.6) {
223
- // Merge section - combine both
224
- result.push(forgeSection.raw);
225
-
226
- // Find and add corresponding existing section
227
- existingSections.forEach((existingSection, idx) => {
228
- if (existingSection.header) {
229
- const existingCategory = detectCategory(existingSection.header);
230
- if (existingCategory.category === 'merge' && existingCategory.confidence > 0.6) {
231
- const normalized1 = forgeSection.header.toLowerCase();
232
- const normalized2 = existingSection.header.toLowerCase();
233
- const distance = levenshteinDistance(normalized1, normalized2);
234
- const similarity = 1 - (distance / Math.max(normalized1.length, normalized2.length));
235
-
236
- if (similarity > 0.5) {
237
- // Add existing content under forge header
238
- result.push('\n' + existingSection.content);
239
- processedExisting.add(idx);
240
- }
241
- }
242
- }
243
- });
244
- }
245
- });
246
-
247
- // Add preserved sections from existing file
248
- existingSections.forEach((section, idx) => {
249
- if (processedExisting.has(idx)) {
250
- return; // Already processed
251
- }
252
-
253
- if (!section.header) {
254
- // Preserve preamble content
255
- if (section.content && section.content.trim() !== '') {
256
- result.unshift(section.raw); // Add to beginning
257
- }
258
- return;
259
- }
260
-
261
- const category = detectCategory(section.header);
262
-
263
- // Preserve sections unless explicitly marked for replacement with high confidence
264
- // Categories: preserve, merge, unknown all get preserved (safety first)
265
- const shouldPreserve = category.category !== 'replace' || category.confidence <= 0.6;
266
-
267
- if (shouldPreserve) {
268
- result.push(section.raw);
269
- }
270
- });
271
-
272
- return result.join('\n\n');
273
- }
274
-
275
- /**
276
- * Semantic merge of existing and forge content
277
- * @param {string} existingContent - Existing file content
278
- * @param {string} forgeContent - Forge template content
279
- * @param {Object} options - { addMarkers: boolean }
280
- * @returns {string} Merged content
281
- */
282
- function semanticMerge(existingContent, forgeContent, options = {}) {
283
- // Normalize line endings to LF for consistent parsing across platforms (Windows CRLF vs Unix LF)
284
- const normalizeLineEndings = (str) => str ? str.replaceAll('\r\n', '\n').replaceAll('\r', '\n') : str;
285
-
286
- existingContent = normalizeLineEndings(existingContent);
287
- forgeContent = normalizeLineEndings(forgeContent);
288
-
289
- // Handle empty cases
290
- if (!existingContent || existingContent.trim() === '') {
291
- return forgeContent || '';
292
- }
293
-
294
- if (!forgeContent || forgeContent.trim() === '') {
295
- return existingContent;
296
- }
297
-
298
- // Parse both documents
299
- const existingSections = parseSemanticSections(existingContent);
300
- const forgeSections = parseSemanticSections(forgeContent);
301
-
302
- // Build merged document
303
- const merged = buildMergedDocument(existingSections, forgeSections, options);
304
-
305
- // Add markers if requested
306
- if (options.addMarkers) {
307
- // Separate preserved (user) and forge sections
308
- const userSections = existingSections.filter(s => {
309
- if (!s.header) return false;
310
- const category = detectCategory(s.header);
311
- return category.category === 'preserve' && category.confidence > 0.6;
312
- });
313
-
314
- const forgeSectionsFiltered = forgeSections.filter(s => {
315
- if (!s.header) return false;
316
- const category = detectCategory(s.header);
317
- return category.category === 'replace' && category.confidence > 0.6;
318
- });
319
-
320
- return wrapWithMarkers({
321
- user: userSections.map(s => s.raw).join('\n\n'),
322
- forge: forgeSectionsFiltered.map(s => s.raw).join('\n\n')
323
- });
324
- }
325
-
326
- return merged;
327
- }
328
-
329
- /**
330
- * Wrap content with USER and FORGE markers
331
- * @param {Object} content - { user: string, forge: string }
332
- * @returns {string} Content wrapped with markers
333
- */
334
- function wrapWithMarkers(content) {
335
- const parts = [];
336
-
337
- if (content.forge && content.forge.trim() !== '') {
338
- parts.push('<!-- FORGE:START -->', content.forge.trim(), '<!-- FORGE:END -->');
339
- }
340
-
341
- if (content.user && content.user.trim() !== '') {
342
- parts.push('', '<!-- USER:START -->', content.user.trim(), '<!-- USER:END -->');
343
- }
344
-
345
- return parts.join('\n');
346
- }
347
-
348
- module.exports = {
349
- parseSemanticSections,
350
- detectCategory,
351
- semanticMerge,
352
- wrapWithMarkers,
353
- // Export for testing
354
- __internal: {
355
- levenshteinDistance,
356
- buildMergedDocument,
357
- SECTION_CATEGORIES
358
- }
359
- };
1
+ /**
2
+ * Semantic Merge for Context Files
3
+ *
4
+ * Intelligently merges existing CLAUDE.md/AGENTS.md files with Forge workflow templates
5
+ * by understanding the semantic meaning of markdown sections.
6
+ */
7
+
8
+ const { distance: levenshteinDistance } = require('fastest-levenshtein');
9
+
10
+ // Section category definitions
11
+ const SECTION_CATEGORIES = {
12
+ preserve: [
13
+ 'Project Description',
14
+ 'Project Instructions',
15
+ 'Project Overview',
16
+ 'Project Background',
17
+ 'Domain Knowledge',
18
+ 'Domain Concepts',
19
+ 'Coding Standards',
20
+ 'Code Standards',
21
+ 'Architecture',
22
+ 'Tech Stack',
23
+ 'Technology Stack',
24
+ 'Build Commands',
25
+ 'Team Conventions',
26
+ 'Migration Strategy',
27
+ 'Setup',
28
+ 'Installation',
29
+ 'Quick Start',
30
+ 'Getting Started'
31
+ ],
32
+
33
+ replace: [
34
+ 'Workflow',
35
+ 'Development Workflow',
36
+ 'Our Workflow',
37
+ 'Workflow Process',
38
+ 'Development Process',
39
+ 'Process',
40
+ 'TDD',
41
+ 'Test-Driven Development',
42
+ 'TDD Approach',
43
+ 'Testing Approach',
44
+ 'Git Workflow',
45
+ 'Git Conventions',
46
+ 'Commit Conventions',
47
+ 'Git Strategy',
48
+ 'Forge Workflow',
49
+ 'Core Principles',
50
+ 'Development Principles'
51
+ ],
52
+
53
+ merge: [
54
+ 'Toolchain',
55
+ 'Tools',
56
+ 'MCP Servers',
57
+ 'Integrations',
58
+ 'Dependencies',
59
+ 'Libraries'
60
+ ]
61
+ };
62
+
63
+ /**
64
+ * Parse markdown content into semantic sections
65
+ * @param {string} markdownContent - Raw markdown content
66
+ * @returns {Array} Array of section objects with structure:
67
+ * { level, header, content, raw, startLine, endLine }
68
+ */
69
+ function parseSemanticSections(markdownContent) {
70
+ if (!markdownContent || typeof markdownContent !== 'string') {
71
+ return [];
72
+ }
73
+
74
+ const lines = markdownContent.split('\n');
75
+ const sections = [];
76
+ let currentSection = null;
77
+
78
+ for (let i = 0; i < lines.length; i++) {
79
+ const line = lines[i];
80
+ // Use RegExp.exec() instead of String.match() per S5852 recommendation
81
+ const headerMatch = /^(#{1,6})\s+([^\r\n]+)$/.exec(line); // NOSONAR S5852 - uses [^\r\n]+ (bounded, no backtracking), developer tool context
82
+
83
+ if (headerMatch) {
84
+ // Save previous section if exists
85
+ if (currentSection) {
86
+ currentSection.endLine = i - 1;
87
+ currentSection.content = currentSection.content.trim();
88
+ sections.push(currentSection);
89
+ }
90
+
91
+ // Start new section
92
+ currentSection = {
93
+ level: headerMatch[1].length,
94
+ header: headerMatch[2].trim(),
95
+ content: '',
96
+ raw: line,
97
+ startLine: i
98
+ };
99
+ } else if (currentSection) {
100
+ // Add content to current section
101
+ currentSection.content += line + '\n';
102
+ currentSection.raw += '\n' + line;
103
+ } else if (line.trim() !== '') {
104
+ // Content before first header (preamble)
105
+ sections.push({
106
+ level: 0,
107
+ header: null,
108
+ content: line,
109
+ raw: line,
110
+ startLine: i,
111
+ endLine: i
112
+ });
113
+ }
114
+ }
115
+
116
+ // Save last section
117
+ if (currentSection) {
118
+ currentSection.endLine = lines.length - 1;
119
+ currentSection.content = currentSection.content.trim();
120
+ sections.push(currentSection);
121
+ }
122
+
123
+ return sections;
124
+ }
125
+
126
+ /**
127
+ * Calculate similarity between normalized text and keyword
128
+ * Reduces cognitive complexity by extracting matching logic (S3776)
129
+ * @param {string} normalized - Normalized header text
130
+ * @param {string} keywordNorm - Normalized keyword
131
+ * @returns {number} - Similarity score 0-1
132
+ */
133
+ function calculateKeywordSimilarity(normalized, keywordNorm) {
134
+ // Fuzzy match using Levenshtein distance
135
+ const distance = levenshteinDistance(normalized, keywordNorm);
136
+ const maxLen = Math.max(normalized.length, keywordNorm.length);
137
+ const similarity = 1 - (distance / maxLen);
138
+
139
+ // Also check if normalized contains the keyword
140
+ if (normalized.includes(keywordNorm) || keywordNorm.includes(normalized)) {
141
+ const containsSimilarity = Math.min(normalized.length, keywordNorm.length) / maxLen;
142
+ return Math.max(similarity, containsSimilarity);
143
+ }
144
+
145
+ return similarity;
146
+ }
147
+
148
+ /**
149
+ * Detect the category of a section based on its header
150
+ * @param {string} headerText - Section header text
151
+ * @returns {Object} { category: 'preserve'|'replace'|'merge'|'unknown', confidence: 0-1 }
152
+ */
153
+ function detectCategory(headerText) {
154
+ if (!headerText || typeof headerText !== 'string') {
155
+ return { category: 'unknown', confidence: 0 };
156
+ }
157
+
158
+ const normalized = headerText.toLowerCase().trim();
159
+ let bestMatch = { category: 'unknown', confidence: 0 };
160
+
161
+ // Check each category
162
+ for (const [category, keywords] of Object.entries(SECTION_CATEGORIES)) {
163
+ for (const keyword of keywords) {
164
+ const keywordNorm = keyword.toLowerCase().trim();
165
+
166
+ // Exact match = highest confidence
167
+ if (normalized === keywordNorm) {
168
+ return { category, confidence: 1 };
169
+ }
170
+
171
+ // Calculate similarity and update best match
172
+ const similarity = calculateKeywordSimilarity(normalized, keywordNorm);
173
+ if (similarity > bestMatch.confidence) {
174
+ bestMatch = { category, confidence: similarity };
175
+ }
176
+ }
177
+ }
178
+
179
+ return bestMatch;
180
+ }
181
+
182
+ /**
183
+ * Build merged document from categorized sections
184
+ * @param {Array} existingSections - Sections from existing file
185
+ * @param {Array} forgeSections - Sections from Forge template
186
+ * @param {Object} options - Merge options
187
+ * @returns {string} Merged markdown content
188
+ */
189
+ function buildMergedDocument(existingSections, forgeSections, _options = {}) {
190
+ const result = [];
191
+ const processedExisting = new Set();
192
+
193
+ // Process forge sections first to establish structure
194
+ forgeSections.forEach(forgeSection => {
195
+ if (!forgeSection.header) {
196
+ return; // Skip preamble from forge
197
+ }
198
+
199
+ const forgeCategory = detectCategory(forgeSection.header);
200
+
201
+ if (forgeCategory.category === 'replace' && forgeCategory.confidence > 0.6) {
202
+ // This is a workflow/TDD section - use forge version
203
+ result.push(forgeSection.raw);
204
+
205
+ // Mark any similar existing sections as processed
206
+ existingSections.forEach((existingSection, idx) => {
207
+ if (existingSection.header) {
208
+ const existingCategory = detectCategory(existingSection.header);
209
+ if (existingCategory.category === 'replace' && existingCategory.confidence > 0.6) {
210
+ // Check if headers are similar enough
211
+ const normalized1 = forgeSection.header.toLowerCase();
212
+ const normalized2 = existingSection.header.toLowerCase();
213
+ const distance = levenshteinDistance(normalized1, normalized2);
214
+ const similarity = 1 - (distance / Math.max(normalized1.length, normalized2.length));
215
+
216
+ if (similarity > 0.5) {
217
+ processedExisting.add(idx);
218
+ }
219
+ }
220
+ }
221
+ });
222
+ } else if (forgeCategory.category === 'merge' && forgeCategory.confidence > 0.6) {
223
+ // Merge section - combine both
224
+ result.push(forgeSection.raw);
225
+
226
+ // Find and add corresponding existing section
227
+ existingSections.forEach((existingSection, idx) => {
228
+ if (existingSection.header) {
229
+ const existingCategory = detectCategory(existingSection.header);
230
+ if (existingCategory.category === 'merge' && existingCategory.confidence > 0.6) {
231
+ const normalized1 = forgeSection.header.toLowerCase();
232
+ const normalized2 = existingSection.header.toLowerCase();
233
+ const distance = levenshteinDistance(normalized1, normalized2);
234
+ const similarity = 1 - (distance / Math.max(normalized1.length, normalized2.length));
235
+
236
+ if (similarity > 0.5) {
237
+ // Add existing content under forge header
238
+ result.push('\n' + existingSection.content);
239
+ processedExisting.add(idx);
240
+ }
241
+ }
242
+ }
243
+ });
244
+ }
245
+ });
246
+
247
+ // Add preserved sections from existing file
248
+ existingSections.forEach((section, idx) => {
249
+ if (processedExisting.has(idx)) {
250
+ return; // Already processed
251
+ }
252
+
253
+ if (!section.header) {
254
+ // Preserve preamble content
255
+ if (section.content && section.content.trim() !== '') {
256
+ result.unshift(section.raw); // Add to beginning
257
+ }
258
+ return;
259
+ }
260
+
261
+ const category = detectCategory(section.header);
262
+
263
+ // Preserve sections unless explicitly marked for replacement with high confidence
264
+ // Categories: preserve, merge, unknown all get preserved (safety first)
265
+ const shouldPreserve = category.category !== 'replace' || category.confidence <= 0.6;
266
+
267
+ if (shouldPreserve) {
268
+ result.push(section.raw);
269
+ }
270
+ });
271
+
272
+ return result.join('\n\n');
273
+ }
274
+
275
+ /**
276
+ * Semantic merge of existing and forge content
277
+ * @param {string} existingContent - Existing file content
278
+ * @param {string} forgeContent - Forge template content
279
+ * @param {Object} options - { addMarkers: boolean }
280
+ * @returns {string} Merged content
281
+ */
282
+ function semanticMerge(existingContent, forgeContent, options = {}) {
283
+ // Normalize line endings to LF for consistent parsing across platforms (Windows CRLF vs Unix LF)
284
+ const normalizeLineEndings = (str) => str ? str.replaceAll('\r\n', '\n').replaceAll('\r', '\n') : str;
285
+
286
+ existingContent = normalizeLineEndings(existingContent);
287
+ forgeContent = normalizeLineEndings(forgeContent);
288
+
289
+ // Handle empty cases
290
+ if (!existingContent || existingContent.trim() === '') {
291
+ return forgeContent || '';
292
+ }
293
+
294
+ if (!forgeContent || forgeContent.trim() === '') {
295
+ return existingContent;
296
+ }
297
+
298
+ // Parse both documents
299
+ const existingSections = parseSemanticSections(existingContent);
300
+ const forgeSections = parseSemanticSections(forgeContent);
301
+
302
+ // Build merged document
303
+ const merged = buildMergedDocument(existingSections, forgeSections, options);
304
+
305
+ // Add markers if requested
306
+ if (options.addMarkers) {
307
+ // Separate preserved (user) and forge sections
308
+ const userSections = existingSections.filter(s => {
309
+ if (!s.header) return false;
310
+ const category = detectCategory(s.header);
311
+ return category.category === 'preserve' && category.confidence > 0.6;
312
+ });
313
+
314
+ const forgeSectionsFiltered = forgeSections.filter(s => {
315
+ if (!s.header) return false;
316
+ const category = detectCategory(s.header);
317
+ return category.category === 'replace' && category.confidence > 0.6;
318
+ });
319
+
320
+ return wrapWithMarkers({
321
+ user: userSections.map(s => s.raw).join('\n\n'),
322
+ forge: forgeSectionsFiltered.map(s => s.raw).join('\n\n')
323
+ });
324
+ }
325
+
326
+ return merged;
327
+ }
328
+
329
+ /**
330
+ * Wrap content with USER and FORGE markers
331
+ * @param {Object} content - { user: string, forge: string }
332
+ * @returns {string} Content wrapped with markers
333
+ */
334
+ function wrapWithMarkers(content) {
335
+ const parts = [];
336
+
337
+ if (content.forge && content.forge.trim() !== '') {
338
+ parts.push('<!-- FORGE:START -->', content.forge.trim(), '<!-- FORGE:END -->');
339
+ }
340
+
341
+ if (content.user && content.user.trim() !== '') {
342
+ parts.push('', '<!-- USER:START -->', content.user.trim(), '<!-- USER:END -->');
343
+ }
344
+
345
+ return parts.join('\n');
346
+ }
347
+
348
+ module.exports = {
349
+ parseSemanticSections,
350
+ detectCategory,
351
+ semanticMerge,
352
+ wrapWithMarkers,
353
+ // Export for testing
354
+ __internal: {
355
+ levenshteinDistance,
356
+ buildMergedDocument,
357
+ SECTION_CATEGORIES
358
+ }
359
+ };