fix: enforce one blank line between concatenated rules#28
Conversation
Jercik
left a comment
There was a problem hiding this comment.
Approach Review: This solves the spacing issue, but there’s a simpler/less stateful way to guarantee exactly one blank line between all adjacent rules: normalize each rule’s content (strip leading + trailing newlines) and then join("\n\n"). That avoids boundary-specific logic and makes the invariant obvious regardless of which rule happens to be first/middle/last.
Approach review by Copilot (gpt-5.2)
src/core/concatenate-rules.ts
Outdated
| } | ||
|
|
||
| let combinedContent = firstRule.content; | ||
| let combinedContent = firstRule.content.replace(/\n+$/u, ""); |
There was a problem hiding this comment.
💡 Alternative approach: Consider normalizing each rule independently and then joining: e.g. return rules.map(r => r.content.replace(/^\n+|\n+$/gu, "")).join("\n\n"); This makes the "exactly one blank line" invariant explicit and avoids per-boundary edge cases.
| } | ||
|
|
||
| combinedContent = `${combinedContent}\n${rule.content}`; | ||
| combinedContent = `${normalizedCombinedContent}\n\n${normalizedRuleContent}`; |
There was a problem hiding this comment.
ℹ️ Info: Clean implementation. The strip-then-rejoin approach handles all the boundary normalization cases well—CRLF, multiple trailing newlines, empty rules. The empty-content guards on lines 23-31 correctly prevent orphan \n\n separators when rules are whitespace-only.
| @@ -13,12 +14,23 @@ export function concatenateRules(rules: Rule[]): string { | |||
| let combinedContent = firstRule.content; | |||
There was a problem hiding this comment.
🟢 Low: The first rule's content is assigned directly without normalization (line 14), which means a single whitespace-only rule like {content: "\n\n"} returns "\n\n" verbatim, while multiple whitespace-only rules collapse to "". The single-rule test on line 12 of the test file confirms this is intentional ("preserves content exactly when only one rule is present"), and in practice rule files won't be pure whitespace, so this is fine. Just noting the asymmetry for awareness—author's discretion on whether to normalize the single-rule path too.
| expect(result).toBe("# A\n\n# B\n\n# C"); | ||
| }); | ||
|
|
||
| it("normalizes CRLF boundaries to one LF blank line", () => { |
There was a problem hiding this comment.
ℹ️ Info: Good addition of the CRLF test case. Verified that the regex \r?\n correctly strips CRLF at boundaries while preserving CRLF within rule content (e.g., "# A\r\nfoo\r\n" joined with "# B" keeps the internal \r\n intact and only normalizes the boundary).
Jercik
left a comment
There was a problem hiding this comment.
Summary: Found 1 medium issue and 1 info-level observation.
Code review by Copilot (gpt-5.2)
| /(?:\r?\n)+$/u, | ||
| "", | ||
| ); | ||
| const normalizedRuleContent = rule.content.replace(/^(?:\r?\n)+/u, ""); |
There was a problem hiding this comment.
🟡 Medium: The empty-rule check only strips leading newlines (rule.content.replace(/^(?:\r?\n)+/u, "")), so a rule file containing only whitespace (e.g., spaces/tabs + newlines) won’t be treated as empty and may introduce stray whitespace into generated AGENTS.md (which can create unintended Markdown hard-breaks). Consider treating rules as empty when normalizedRuleContent.trim() === "" (or equivalent whitespace-only check) before concatenation.
| if (combinedContent.endsWith("\n") || rule.content.startsWith("\n")) { | ||
| combinedContent = `${combinedContent}${rule.content}`; | ||
| const normalizedCombinedContent = combinedContent.replace( | ||
| /(?:\r?\n)+$/u, |
There was a problem hiding this comment.
ℹ️ Info: Good boundary normalization: trimming trailing/leading newline runs with /(?:\r?\n)+$/u and joining with \n\n makes spacing deterministic and also normalizes CRLF input to LF at the join points (covered by the new CRLF test in src/core/concatenate-rules.test.ts).
|
🎉 This PR is included in version 5.9.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Summary
Testing