Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions internal/sync/reference/defensive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,11 @@ func TestStructuredAdaptersRejectMalformedSources(t *testing.T) {
})
}
}

func TestMarkdownProfilesRejectUnsafeMarkers(t *testing.T) {
_, err := parseMarkdownProfile(
Document{Path: "instructions.md", Data: []byte("<!-- launchdarkly:begin one -->\n")},
false,
)
require.Error(t, err)
}
81 changes: 81 additions & 0 deletions internal/sync/reference/profile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package reference

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

syncdomain "github.com/launchdarkly/ldcli/internal/sync"
)

func TestMarkdownProfilesPreserveActivationMetadata(t *testing.T) {
tests := []struct {
name, fixture, realPath, format, metadataPath string
metadataValue any
}{
{name: "Cursor", fixture: "profiles/cursor-rule.mdc", realPath: ".cursor/rules/support.mdc", format: "cursor", metadataPath: "frontmatter.globs", metadataValue: "src/support/**/*.ts"},
{name: "Copilot", fixture: "profiles/copilot.instructions.md", realPath: ".github/instructions/support.instructions.md", format: "copilot", metadataPath: "frontmatter.applyTo", metadataValue: "src/support/**/*.ts"},
{name: "Claude", fixture: "profiles/claude-instructions.md.golden", realPath: "CLAUDE.md", format: "claude-markdown", metadataPath: "frontmatter.model", metadataValue: "sonnet"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
documentPath, source := materializeFixture(t, test.fixture, test.realPath)
id := syncdomain.VariationID("project", "config", "default")
reference := syncdomain.Reference{File: documentPath, Format: test.format}
root, err := parseSourceDocument(Document{Path: documentPath, Data: source}, test.format)
require.NoError(t, err)
metadata, err := resolveSourcePath(root, test.metadataPath)
require.NoError(t, err)
assert.Equal(t, test.metadataValue, metadata.value)

projection, err := ParseSource(sourceBundle(documentPath, source), id, syncdomain.VariationModeAgent, reference)
require.NoError(t, err)
prompt := "Updated profile instructions."
result, err := UpdateSource(
sourceBundle(documentPath, source), id, syncdomain.VariationModeAgent, reference,
syncdomain.VariationProjection{Mode: syncdomain.VariationModeAgent, Instructions: &prompt},
)
require.NoError(t, err)
updated := applyConformanceEdits(t, source, directEdits(t, result))
root, err = parseSourceDocument(Document{Path: documentPath, Data: updated}, test.format)
require.NoError(t, err)
metadata, err = resolveSourcePath(root, test.metadataPath)
require.NoError(t, err)
assert.Equal(t, test.metadataValue, metadata.value)
assert.NotNil(t, projection)
})
}
}

func TestAgentsMarkdownSectionsAndMarkers(t *testing.T) {
documentPath, source := materializeFixture(t, "profiles/agents-instructions.md.golden", "AGENTS.md")
id := syncdomain.VariationID("project", "config", "default")
tests := []struct {
name, selectPath, mapPath, want string
}{
{name: "section", selectPath: `sections[heading="Support"]`, mapPath: "body", want: "Ask for an order ID.\n\n<!-- launchdarkly:begin refunds -->\nExplain the refund policy.\n<!-- launchdarkly:end refunds -->"},
{name: "marker", selectPath: `markers[id="refunds"]`, mapPath: "body", want: "Explain the refund policy."},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
reference := syncdomain.Reference{
File: documentPath, Format: "agents-markdown", Select: test.selectPath,
Map: map[string]string{"prompt": test.mapPath},
}
projection, err := ParseSource(
sourceBundle(reference.File, source), id, syncdomain.VariationModeAgent, reference,
)
require.NoError(t, err)
assert.Equal(t, test.want, *projection.(syncdomain.VariationProjection).Instructions)
prompt := "Updated selected instructions."
result, err := UpdateSource(
sourceBundle(reference.File, source), id, syncdomain.VariationModeAgent, reference,
syncdomain.VariationProjection{Mode: syncdomain.VariationModeAgent, Instructions: &prompt},
)
require.NoError(t, err)
updated := applyConformanceEdits(t, source, directEdits(t, result))
assert.Contains(t, string(updated), "## Build\n\nRun `make test`.")
})
}
}
126 changes: 126 additions & 0 deletions internal/sync/reference/profiles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package reference

import (
"bytes"
"fmt"
"strings"

"gopkg.in/yaml.v3"

syncdomain "github.com/launchdarkly/ldcli/internal/sync"
)

type markdownProfileAdapter struct {
format string
frontMatter bool
}

func (adapter markdownProfileAdapter) Format() string { return adapter.format }
func (adapter markdownProfileAdapter) Inspect(bundle SourceBundle) (Inspection, error) {
root, err := parseMarkdownProfile(bundle.Primary, adapter.frontMatter)
return Inspection{root: root}, err
}
func (markdownProfileAdapter) Defaults(kind syncdomain.Kind) (string, map[string]string) {
if kind == syncdomain.KindVariation {
return "$", map[string]string{string(syncdomain.FieldPrompt): "body"}
}
return "$", nil
}

func init() {
mustRegister(markdownProfileAdapter{format: "cursor", frontMatter: true})
mustRegister(markdownProfileAdapter{format: "copilot", frontMatter: true})
mustRegister(markdownProfileAdapter{format: "agents-markdown"})
mustRegister(markdownProfileAdapter{format: "claude-markdown", frontMatter: true})
}

func parseMarkdownProfile(document Document, supportsFrontMatter bool) (*sourceNode, error) {
data := document.Data
bodyStart := 0
frontmatter := &sourceNode{value: map[string]any{}, fields: map[string]*sourceNode{}}
trimmed := bytes.TrimPrefix(data, []byte("\ufeff"))
bomLength := len(data) - len(trimmed)
if supportsFrontMatter && bytes.HasPrefix(trimmed, []byte("---")) {
region, start, err := splitFrontMatter(data)
if err != nil {
return nil, err
}
bodyStart = start
var documentNode yaml.Node
if err := yaml.Unmarshal(region.data, &documentNode); err != nil {
return nil, fmt.Errorf("invalid profile front matter: %w", err)
}
if len(documentNode.Content) != 0 {
frontmatter, err = yamlSourceNode(region.data, documentNode.Content[0], region.startOffset)
if err != nil {
return nil, err
}
}
} else {
bodyStart = bomLength
}
bodyData := data[bodyStart:]
body := markdownTextNode(bodyData, bodyStart, len(data))
sections := &sourceNode{items: markdownSections(bodyData), collection: true}
offsetSourceSpans(sections, bodyStart)
markers, err := markdownMarkers(bodyData, bodyStart)
if err != nil {
return nil, err
}
return &sourceNode{fields: map[string]*sourceNode{
"frontmatter": frontmatter,
"body": body,
"sections": sections,
"markers": markers,
}}, nil
}

func markdownMarkers(data []byte, base int) (*sourceNode, error) {
const beginPrefix = "<!-- launchdarkly:begin "
const endPrefix = "<!-- launchdarkly:end "
var items []*sourceNode
openID, bodyStart := "", 0
for offset := 0; offset < len(data); {
lineEnd := nextLineEnd(data, offset)
line := strings.TrimSpace(string(data[offset:lineEnd]))
switch {
case strings.HasPrefix(line, beginPrefix) && strings.HasSuffix(line, "-->"):
if openID != "" {
return nil, fmt.Errorf("nested LaunchDarkly marker %q", openID)
}
openID = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, beginPrefix), "-->"))
bodyStart = lineEnd
case strings.HasPrefix(line, endPrefix) && strings.HasSuffix(line, "-->"):
id := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, endPrefix), "-->"))
if openID == "" || id != openID {
return nil, fmt.Errorf("LaunchDarkly marker %q has no matching begin marker", id)
}
body := markdownTextNode(data[bodyStart:offset], base+bodyStart, base+offset)
items = append(items, &sourceNode{fields: map[string]*sourceNode{
"id": {value: id}, "body": body,
}})
openID = ""
}
offset = lineEnd
}
if openID != "" {
return nil, fmt.Errorf("LaunchDarkly marker %q is not closed", openID)
}
return &sourceNode{items: items, collection: true}, nil
}

func offsetSourceSpans(node *sourceNode, offset int) {
if node == nil {
return
}
if node.start != 0 || node.end != 0 {
node.start += offset
node.end += offset
}
for _, child := range node.fields {
offsetSourceSpans(child, offset)
}
for _, child := range node.items {
offsetSourceSpans(child, offset)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Repository guide

## Support

Ask for an order ID.

<!-- launchdarkly:begin refunds -->
Explain the refund policy.
<!-- launchdarkly:end refunds -->

## Build

Run `make test`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: support
description: Support subagent instructions
tools: Read, Grep
model: sonnet
---

# Support subagent

Ask for an order ID and follow the support policy.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
applyTo: "src/support/**/*.ts"
---

# Support

Use the support policy.
9 changes: 9 additions & 0 deletions internal/sync/reference/testdata/profiles/cursor-rule.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
description: Support instructions
globs: "src/support/**/*.ts"
alwaysApply: false
---

# Support

Ask for an order ID before discussing an order.
Loading