@tanagram/cli 0.1.7 → 0.1.9

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/README.md CHANGED
@@ -90,16 +90,40 @@ tanagram help
90
90
 
91
91
  **Smart Caching:** Policies are cached and automatically resynced when instruction files change (detected via MD5 hash).
92
92
 
93
- ## Commands
93
+ ### Commands
94
94
 
95
95
  - **`run`** (default) - Check git changes against policies with auto-sync
96
96
  - **`sync`** - Manually sync all instruction files to cache
97
97
  - **`list`** - View all cached policies (shows enforceable vs unenforceable)
98
98
  - **`help`** - Show usage information
99
99
 
100
+ ### Claude Code Hook
101
+ You can install the CLI as a Claude Code [hook](https://code.claude.com/docs/en/hooks) to have Claude automatically iterate on Tanagram's output. Add the following to your `~/.claude/settings.json` (user settings) or `.claude/settings.json` (project settings):
102
+
103
+ ```json
104
+ {
105
+ "hooks": {
106
+ "PostToolUse": [
107
+ {
108
+ "matcher": "Edit|Write",
109
+ "hooks": [
110
+ {
111
+ "type": "command",
112
+ "command": "tanagram"
113
+ }
114
+ ]
115
+ }
116
+ ]
117
+ }
118
+ }
119
+ ```
120
+
121
+ If you have existing hooks, you can merge this hook into your existing config.
122
+
123
+
100
124
  ## How It Works
101
125
 
102
- 1. **Finds instruction files** - Searches for `AGENTS.md`, `POLICIES.md`, `CLAUDE.md` in your git repository
126
+ 1. **Finds instruction files** - Searches for `AGENTS.md`, `POLICIES.md`, `CLAUDE.md`, and `.cursor/rules/*.mdc` in your git repository
103
127
  2. **Checks cache** - Loads cached policies and MD5 hashes from `.tanagram/`
104
128
  3. **Auto-syncs** - Detects file changes via MD5 and automatically resyncs if needed
105
129
  4. **LLM extraction** - Uses Claude AI to extract ALL policies from instruction files
@@ -157,7 +181,7 @@ Claude AI analyzes code changes against all policies:
157
181
  ## Exit Codes
158
182
 
159
183
  - `0` - No violations found
160
- - `1` - Violations found (fails CI/CD if integrated)
184
+ - `2` - Violations found (triggers Claude Code automatic fix behavior)
161
185
 
162
186
  ## Example
163
187
 
@@ -171,8 +195,25 @@ Create an `AGENTS.md` in your repo with policies:
171
195
  - Always use async/await for database operations
172
196
  ```
173
197
 
198
+ Or use Cursor rules files in `.cursor/rules/`:
199
+
200
+ ```markdown
201
+ ---
202
+ description: TypeScript coding standards
203
+ globs: ["*.ts", "*.tsx"]
204
+ ---
205
+
206
+ # TypeScript Standards
207
+
208
+ - Use strict type checking
209
+ - Avoid using 'any' type
210
+ - Prefer interfaces over type aliases
211
+ ```
212
+
174
213
  Then run `tanagram` to enforce them locally!
175
214
 
215
+ **Note:** For `.mdc` files, Tanagram extracts policies from the markdown content only (YAML frontmatter is used by Cursor and ignored during policy extraction).
216
+
176
217
  ---
177
218
 
178
219
  ## Contributing
package/bin/tanagram CHANGED
Binary file
@@ -21,7 +21,7 @@ type Violation struct {
21
21
 
22
22
  // CheckResult contains all violations found
23
23
  type CheckResult struct {
24
- Violations []Violation
24
+ Violations []Violation
25
25
  TotalChecked int
26
26
  }
27
27
 
@@ -89,3 +89,52 @@ func FormatViolations(result *CheckResult) string {
89
89
 
90
90
  return output.String()
91
91
  }
92
+
93
+ // FormatClaudeInstructions formats violations as actionable instructions for Claude Code
94
+ // to automatically fix via exit code 2 hook behavior
95
+ func FormatClaudeInstructions(result *CheckResult) string {
96
+ if len(result.Violations) == 0 {
97
+ return ""
98
+ }
99
+
100
+ // Group violations by file
101
+ violationsByFile := make(map[string][]Violation)
102
+ for _, v := range result.Violations {
103
+ violationsByFile[v.File] = append(violationsByFile[v.File], v)
104
+ }
105
+
106
+ // Sort files for consistent output
107
+ files := make([]string, 0, len(violationsByFile))
108
+ for file := range violationsByFile {
109
+ files = append(files, file)
110
+ }
111
+ sort.Strings(files)
112
+
113
+ var output strings.Builder
114
+ output.WriteString("POLICY VIOLATIONS DETECTED - PLEASE FIX\n\n")
115
+ output.WriteString(fmt.Sprintf("Found %d policy violation(s) that need to be fixed:\n\n", len(result.Violations)))
116
+
117
+ for _, file := range files {
118
+ violations := violationsByFile[file]
119
+
120
+ // Sort violations by line number
121
+ sort.Slice(violations, func(i, j int) bool {
122
+ return violations[i].LineNumber < violations[j].LineNumber
123
+ })
124
+
125
+ output.WriteString(fmt.Sprintf("File: %s\n", file))
126
+
127
+ for _, v := range violations {
128
+ output.WriteString(fmt.Sprintf("\n Line %d:\n", v.LineNumber))
129
+ output.WriteString(fmt.Sprintf(" Code: %s\n", v.Code))
130
+ output.WriteString(fmt.Sprintf(" Policy: %s\n", v.PolicyName))
131
+ output.WriteString(fmt.Sprintf(" Issue: %s\n", v.Message))
132
+ output.WriteString(fmt.Sprintf(" Action: Review and fix this code to comply with the policy.\n"))
133
+ }
134
+ output.WriteString("\n")
135
+ }
136
+
137
+ output.WriteString("Please fix all violations listed above and ensure the code complies with all policies.\n")
138
+
139
+ return output.String()
140
+ }
@@ -0,0 +1,215 @@
1
+ package checker
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ func TestFormatViolations_NoViolations(t *testing.T) {
9
+ result := &CheckResult{
10
+ Violations: []Violation{},
11
+ TotalChecked: 10,
12
+ }
13
+
14
+ output := FormatViolations(result)
15
+ expected := "✓ No policy violations found"
16
+
17
+ if output != expected {
18
+ t.Errorf("Expected %q, got %q", expected, output)
19
+ }
20
+ }
21
+
22
+ func TestFormatViolations_WithViolations(t *testing.T) {
23
+ result := &CheckResult{
24
+ Violations: []Violation{
25
+ {
26
+ File: "main.go",
27
+ LineNumber: 10,
28
+ PolicyName: "No hardcoded secrets",
29
+ Message: "Potential hardcoded API key detected",
30
+ Code: "apiKey := \"sk-1234567890\"",
31
+ },
32
+ {
33
+ File: "config.go",
34
+ LineNumber: 5,
35
+ PolicyName: "Use environment variables",
36
+ Message: "Configuration should use environment variables",
37
+ Code: "var token = \"abc123\"",
38
+ },
39
+ },
40
+ TotalChecked: 50,
41
+ }
42
+
43
+ output := FormatViolations(result)
44
+
45
+ // Check that output contains expected elements
46
+ if !strings.Contains(output, "✗ Found 2 policy violation(s)") {
47
+ t.Errorf("Expected violation count in output, got: %s", output)
48
+ }
49
+ if !strings.Contains(output, "main.go") {
50
+ t.Errorf("Expected file name 'main.go' in output")
51
+ }
52
+ if !strings.Contains(output, "config.go") {
53
+ t.Errorf("Expected file name 'config.go' in output")
54
+ }
55
+ if !strings.Contains(output, "10:") {
56
+ t.Errorf("Expected line number 10 in output")
57
+ }
58
+ if !strings.Contains(output, "Potential hardcoded API key detected") {
59
+ t.Errorf("Expected violation message in output")
60
+ }
61
+ }
62
+
63
+ func TestFormatClaudeInstructions_NoViolations(t *testing.T) {
64
+ result := &CheckResult{
65
+ Violations: []Violation{},
66
+ TotalChecked: 10,
67
+ }
68
+
69
+ output := FormatClaudeInstructions(result)
70
+
71
+ if output != "" {
72
+ t.Errorf("Expected empty string for no violations, got: %s", output)
73
+ }
74
+ }
75
+
76
+ func TestFormatClaudeInstructions_WithViolations(t *testing.T) {
77
+ result := &CheckResult{
78
+ Violations: []Violation{
79
+ {
80
+ File: "main.go",
81
+ LineNumber: 10,
82
+ PolicyName: "No hardcoded secrets",
83
+ Message: "Potential hardcoded API key detected",
84
+ Code: "apiKey := \"sk-1234567890\"",
85
+ },
86
+ {
87
+ File: "main.go",
88
+ LineNumber: 15,
89
+ PolicyName: "No hardcoded secrets",
90
+ Message: "Potential hardcoded password detected",
91
+ Code: "password := \"secret123\"",
92
+ },
93
+ {
94
+ File: "config.go",
95
+ LineNumber: 5,
96
+ PolicyName: "Use environment variables",
97
+ Message: "Configuration should use environment variables",
98
+ Code: "var token = \"abc123\"",
99
+ },
100
+ },
101
+ TotalChecked: 50,
102
+ }
103
+
104
+ output := FormatClaudeInstructions(result)
105
+
106
+ // Check header
107
+ if !strings.Contains(output, "POLICY VIOLATIONS DETECTED - PLEASE FIX") {
108
+ t.Errorf("Expected header in output")
109
+ }
110
+
111
+ // Check violation count
112
+ if !strings.Contains(output, "Found 3 policy violation(s)") {
113
+ t.Errorf("Expected violation count in output, got: %s", output)
114
+ }
115
+
116
+ // Check file names
117
+ if !strings.Contains(output, "File: main.go") {
118
+ t.Errorf("Expected 'File: main.go' in output")
119
+ }
120
+ if !strings.Contains(output, "File: config.go") {
121
+ t.Errorf("Expected 'File: config.go' in output")
122
+ }
123
+
124
+ // Check line numbers
125
+ if !strings.Contains(output, "Line 10:") {
126
+ t.Errorf("Expected 'Line 10:' in output")
127
+ }
128
+ if !strings.Contains(output, "Line 15:") {
129
+ t.Errorf("Expected 'Line 15:' in output")
130
+ }
131
+ if !strings.Contains(output, "Line 5:") {
132
+ t.Errorf("Expected 'Line 5:' in output")
133
+ }
134
+
135
+ // Check policy names
136
+ if !strings.Contains(output, "Policy: No hardcoded secrets") {
137
+ t.Errorf("Expected policy name in output")
138
+ }
139
+ if !strings.Contains(output, "Policy: Use environment variables") {
140
+ t.Errorf("Expected policy name in output")
141
+ }
142
+
143
+ // Check code snippets
144
+ if !strings.Contains(output, "Code: apiKey := \"sk-1234567890\"") {
145
+ t.Errorf("Expected code snippet in output")
146
+ }
147
+
148
+ // Check issue messages
149
+ if !strings.Contains(output, "Issue: Potential hardcoded API key detected") {
150
+ t.Errorf("Expected issue message in output")
151
+ }
152
+
153
+ // Check action instruction
154
+ if !strings.Contains(output, "Action: Review and fix this code to comply with the policy.") {
155
+ t.Errorf("Expected action instruction in output")
156
+ }
157
+
158
+ // Check footer
159
+ if !strings.Contains(output, "Please fix all violations listed above and ensure the code complies with all policies.") {
160
+ t.Errorf("Expected footer instruction in output")
161
+ }
162
+ }
163
+
164
+ func TestFormatClaudeInstructions_MultipleFilesGroupedAndSorted(t *testing.T) {
165
+ result := &CheckResult{
166
+ Violations: []Violation{
167
+ {
168
+ File: "z_file.go",
169
+ LineNumber: 20,
170
+ PolicyName: "Policy Z",
171
+ Message: "Issue Z",
172
+ Code: "code z",
173
+ },
174
+ {
175
+ File: "a_file.go",
176
+ LineNumber: 30,
177
+ PolicyName: "Policy A2",
178
+ Message: "Issue A2",
179
+ Code: "code a2",
180
+ },
181
+ {
182
+ File: "a_file.go",
183
+ LineNumber: 10,
184
+ PolicyName: "Policy A1",
185
+ Message: "Issue A1",
186
+ Code: "code a1",
187
+ },
188
+ },
189
+ TotalChecked: 50,
190
+ }
191
+
192
+ output := FormatClaudeInstructions(result)
193
+
194
+ // Files should be sorted alphabetically
195
+ aFileIndex := strings.Index(output, "File: a_file.go")
196
+ zFileIndex := strings.Index(output, "File: z_file.go")
197
+
198
+ if aFileIndex == -1 || zFileIndex == -1 {
199
+ t.Errorf("Expected both file names in output")
200
+ }
201
+ if aFileIndex >= zFileIndex {
202
+ t.Errorf("Expected files to be sorted alphabetically (a_file.go before z_file.go)")
203
+ }
204
+
205
+ // Within a file, violations should be sorted by line number
206
+ line10Index := strings.Index(output, "Line 10:")
207
+ line30Index := strings.Index(output, "Line 30:")
208
+
209
+ if line10Index == -1 || line30Index == -1 {
210
+ t.Errorf("Expected both line numbers in output")
211
+ }
212
+ if line10Index >= line30Index {
213
+ t.Errorf("Expected violations to be sorted by line number (10 before 30)")
214
+ }
215
+ }
package/commands/run.go CHANGED
@@ -47,7 +47,7 @@ func Run() error {
47
47
  }
48
48
 
49
49
  if len(instructionFiles) == 0 {
50
- return fmt.Errorf("no instruction files found (looking for AGENTS.md, POLICIES.md, CLAUDE.md)")
50
+ return fmt.Errorf("no instruction files found (looking for AGENTS.md, POLICIES.md, CLAUDE.md, .cursor/rules/*.mdc)")
51
51
  }
52
52
 
53
53
  // Load cache
@@ -196,14 +196,21 @@ func Run() error {
196
196
  ctx := context.Background()
197
197
  result := checker.CheckChanges(ctx, diffResult.Changes, policies)
198
198
 
199
- // Output results
200
- output := checker.FormatViolations(result)
201
- fmt.Print(output)
202
-
203
- // Exit with error if violations found
199
+ // Handle results based on whether violations were found
204
200
  if len(result.Violations) > 0 {
205
- os.Exit(1)
201
+ // Format and output violations to stderr
202
+ violationOutput := checker.FormatViolations(result)
203
+ fmt.Fprint(os.Stderr, violationOutput)
204
+
205
+ // Format and output Claude-friendly instructions to stderr
206
+ claudeInstructions := checker.FormatClaudeInstructions(result)
207
+ fmt.Fprint(os.Stderr, claudeInstructions)
208
+
209
+ // Exit with code 2 to trigger Claude Code hook behavior
210
+ os.Exit(2)
206
211
  }
207
212
 
213
+ // No violations found - output success message
214
+ fmt.Println("✓ No policy violations found")
208
215
  return nil
209
216
  }
package/commands/sync.go CHANGED
@@ -28,7 +28,7 @@ func Sync() error {
28
28
  }
29
29
 
30
30
  if len(instructionFiles) == 0 {
31
- return fmt.Errorf("no instruction files found (looking for AGENTS.md, POLICIES.md, CLAUDE.md)")
31
+ return fmt.Errorf("no instruction files found (looking for AGENTS.md, POLICIES.md, CLAUDE.md, .cursor/rules/*.mdc)")
32
32
  }
33
33
 
34
34
  fmt.Printf("Found %d instruction file(s)\n", len(instructionFiles))
@@ -137,7 +137,7 @@ func Sync() error {
137
137
  }
138
138
 
139
139
  // FindInstructionFiles searches for instruction files in the git repository
140
- // Looks for: AGENTS.md, POLICIES.md, CLAUDE.md
140
+ // Looks for: AGENTS.md, POLICIES.md, CLAUDE.md, and .cursor/rules/*.mdc
141
141
  func FindInstructionFiles(gitRoot string) ([]string, error) {
142
142
  var files []string
143
143
 
@@ -175,12 +175,22 @@ func FindInstructionFiles(gitRoot string) ([]string, error) {
175
175
 
176
176
  // Check if this is one of our instruction files
177
177
  if !info.IsDir() {
178
+ // Check for common instruction files (AGENTS.md, etc.)
178
179
  for _, name := range commonNames {
179
180
  if info.Name() == name {
180
181
  files = append(files, path)
181
182
  break
182
183
  }
183
184
  }
185
+
186
+ // Check for .mdc files in .cursor/rules/ directory
187
+ if filepath.Ext(info.Name()) == ".mdc" {
188
+ // Check if this file is inside .cursor/rules/
189
+ relPath, err := filepath.Rel(gitRoot, path)
190
+ if err == nil && filepath.HasPrefix(relPath, filepath.Join(".cursor", "rules")) {
191
+ files = append(files, path)
192
+ }
193
+ }
184
194
  }
185
195
 
186
196
  return nil
package/extractor/file.go CHANGED
@@ -4,16 +4,25 @@ import (
4
4
  "context"
5
5
  "fmt"
6
6
  "os"
7
+ "path/filepath"
8
+ "strings"
7
9
 
8
10
  "github.com/tanagram/cli/parser"
9
11
  )
10
12
 
11
13
  // ExtractPoliciesFromFile reads a file and extracts policies using LLM
12
- func ExtractPoliciesFromFile(ctx context.Context, filepath string) ([]parser.Policy, error) {
13
- content, err := os.ReadFile(filepath)
14
+ func ExtractPoliciesFromFile(ctx context.Context, filePath string) ([]parser.Policy, error) {
15
+ content, err := os.ReadFile(filePath)
14
16
  if err != nil {
15
17
  return nil, fmt.Errorf("failed to read file: %w", err)
16
18
  }
17
19
 
18
- return ExtractPolicies(ctx, string(content))
20
+ contentStr := string(content)
21
+
22
+ // If this is an .mdc file, strip YAML frontmatter
23
+ if strings.HasSuffix(strings.ToLower(filepath.Base(filePath)), ".mdc") {
24
+ contentStr = parser.StripMDCFrontmatter(contentStr)
25
+ }
26
+
27
+ return ExtractPolicies(ctx, contentStr)
19
28
  }
package/main.go CHANGED
@@ -56,9 +56,11 @@ EXAMPLES:
56
56
  tanagram list # View all cached policies
57
57
 
58
58
  INSTRUCTION FILES:
59
- Tanagram looks for instruction files like AGENTS.md, POLICIES.md, or CLAUDE.md
60
- in your git repository. Policies are cached and automatically resynced
61
- when files change.
59
+ Tanagram looks for instruction files in your git repository:
60
+ - AGENTS.md, POLICIES.md, CLAUDE.md
61
+ - Cursor rules: .cursor/rules/*.mdc
62
+
63
+ Policies are cached and automatically resynced when files change.
62
64
  `
63
65
  fmt.Print(help)
64
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {
package/parser/mdc.go ADDED
@@ -0,0 +1,57 @@
1
+ package parser
2
+
3
+ import (
4
+ "bufio"
5
+ "strings"
6
+ )
7
+
8
+ // StripMDCFrontmatter removes YAML frontmatter from .mdc files
9
+ // .mdc files have optional YAML frontmatter between --- delimiters
10
+ // We only need the markdown content for policy extraction
11
+ func StripMDCFrontmatter(content string) string {
12
+ lines := strings.Split(content, "\n")
13
+
14
+ // Check if file starts with frontmatter delimiter
15
+ if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
16
+ // No frontmatter, return as-is
17
+ return content
18
+ }
19
+
20
+ // Find the closing frontmatter delimiter
21
+ scanner := bufio.NewScanner(strings.NewReader(content))
22
+ inFrontmatter := false
23
+ frontmatterEnded := false
24
+ lineNum := 0
25
+
26
+ var markdownLines []string
27
+
28
+ for scanner.Scan() {
29
+ line := scanner.Text()
30
+ lineNum++
31
+
32
+ // First line is opening ---
33
+ if lineNum == 1 && strings.TrimSpace(line) == "---" {
34
+ inFrontmatter = true
35
+ continue
36
+ }
37
+
38
+ // Look for closing ---
39
+ if inFrontmatter && strings.TrimSpace(line) == "---" {
40
+ inFrontmatter = false
41
+ frontmatterEnded = true
42
+ continue
43
+ }
44
+
45
+ // Skip frontmatter content
46
+ if inFrontmatter {
47
+ continue
48
+ }
49
+
50
+ // After frontmatter ends, collect markdown lines
51
+ if frontmatterEnded || lineNum > 1 {
52
+ markdownLines = append(markdownLines, line)
53
+ }
54
+ }
55
+
56
+ return strings.Join(markdownLines, "\n")
57
+ }
@@ -0,0 +1,137 @@
1
+ package parser
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ func TestStripMDCFrontmatter_WithFrontmatter(t *testing.T) {
9
+ input := `---
10
+ description: TypeScript coding standards
11
+ globs: ["*.ts", "*.tsx"]
12
+ alwaysApply: false
13
+ ---
14
+
15
+ # TypeScript Standards
16
+
17
+ - Use strict type checking
18
+ - Avoid using 'any' type`
19
+
20
+ expected := `
21
+ # TypeScript Standards
22
+
23
+ - Use strict type checking
24
+ - Avoid using 'any' type`
25
+
26
+ result := StripMDCFrontmatter(input)
27
+
28
+ if result != expected {
29
+ t.Errorf("Expected:\n%s\n\nGot:\n%s", expected, result)
30
+ }
31
+ }
32
+
33
+ func TestStripMDCFrontmatter_NoFrontmatter(t *testing.T) {
34
+ input := `# TypeScript Standards
35
+
36
+ - Use strict type checking
37
+ - Avoid using 'any' type`
38
+
39
+ result := StripMDCFrontmatter(input)
40
+
41
+ if result != input {
42
+ t.Errorf("Expected input to be unchanged when no frontmatter present")
43
+ }
44
+ }
45
+
46
+ func TestStripMDCFrontmatter_EmptyFile(t *testing.T) {
47
+ input := ""
48
+ result := StripMDCFrontmatter(input)
49
+
50
+ if result != input {
51
+ t.Errorf("Expected empty string for empty input")
52
+ }
53
+ }
54
+
55
+ func TestStripMDCFrontmatter_OnlyFrontmatter(t *testing.T) {
56
+ input := `---
57
+ description: Test rule
58
+ globs: ["*.ts"]
59
+ ---`
60
+
61
+ result := StripMDCFrontmatter(input)
62
+
63
+ // Should return empty or minimal content after frontmatter
64
+ if strings.TrimSpace(result) != "" {
65
+ t.Errorf("Expected empty content after stripping frontmatter-only file, got: %s", result)
66
+ }
67
+ }
68
+
69
+ func TestStripMDCFrontmatter_ComplexFrontmatter(t *testing.T) {
70
+ input := `---
71
+ description: Multi-line description
72
+ that spans multiple lines
73
+ globs:
74
+ - "src/**/*.ts"
75
+ - "test/**/*.test.ts"
76
+ alwaysApply: false
77
+ priority: 10
78
+ dependencies:
79
+ - other-rule.mdc
80
+ ---
81
+
82
+ # Complex Rule
83
+
84
+ ## Section 1
85
+ - Policy 1
86
+ - Policy 2
87
+
88
+ ## Section 2
89
+ - Policy 3`
90
+
91
+ result := StripMDCFrontmatter(input)
92
+
93
+ // Should contain markdown but not frontmatter
94
+ if !strings.Contains(result, "# Complex Rule") {
95
+ t.Errorf("Expected markdown content to be preserved")
96
+ }
97
+ if strings.Contains(result, "description:") {
98
+ t.Errorf("Expected frontmatter to be removed")
99
+ }
100
+ if strings.Contains(result, "globs:") {
101
+ t.Errorf("Expected frontmatter to be removed")
102
+ }
103
+ }
104
+
105
+ func TestStripMDCFrontmatter_MarkdownWithCodeBlocks(t *testing.T) {
106
+ input := `---
107
+ description: Code examples
108
+ ---
109
+
110
+ # Rules
111
+
112
+ Example with --- in code:
113
+
114
+ ` + "```" + `yaml
115
+ ---
116
+ some: yaml
117
+ ---
118
+ ` + "```" + `
119
+
120
+ - Follow this policy`
121
+
122
+ result := StripMDCFrontmatter(input)
123
+
124
+ // Should preserve --- inside code blocks
125
+ if !strings.Contains(result, "```yaml") {
126
+ t.Errorf("Expected code blocks to be preserved")
127
+ }
128
+ if !strings.Contains(result, "- Follow this policy") {
129
+ t.Errorf("Expected policy content to be preserved")
130
+ }
131
+ // First frontmatter should be removed
132
+ lines := strings.Split(result, "\n")
133
+ if len(lines) > 0 && strings.TrimSpace(lines[0]) == "---" {
134
+ // This is tricky - the first --- should be gone, but ones in code blocks should remain
135
+ // For now, we accept this limitation as our parser is simple
136
+ }
137
+ }