Skip to content

Commit 5d44ffc

Browse files
committed
refactor: extract shared SplitFrontmatter, delete 4 dead delimiter errors
Extract duplicated frontmatter splitting logic from steering/ and skill/ into parse.SplitFrontmatter. Both callers now delegate to the shared function and wrap errors with domain-specific context. Delete 4 dead error constructors (MissingOpeningDelimiter, MissingClosingDelimiter in err/steering and err/skill) and their DescKey constants + YAML entries. Add new err/parser delimiter constructors used by the shared function. Spec: specs/ast-audit-contributor-guide.md Signed-off-by: Jose Alekhinne <jose@ctx.ist>
1 parent 0819067 commit 5d44ffc

11 files changed

Lines changed: 100 additions & 142 deletions

File tree

internal/assets/commands/text/errors.yaml

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -446,12 +446,8 @@ err.skill.invalid-yaml:
446446
short: 'skill: %s: invalid YAML frontmatter: %w'
447447
err.skill.load:
448448
short: 'skill: %s: %w'
449-
err.skill.missing-closing-delimiter:
450-
short: missing closing frontmatter delimiter (---)
451449
err.skill.missing-name:
452450
short: 'skill: %s is missing required ''name'' field'
453-
err.skill.missing-opening-delimiter:
454-
short: missing opening frontmatter delimiter (---)
455451
err.skill.not-found:
456452
short: 'skill %q not found'
457453
err.skill.not-valid-dir:
@@ -478,10 +474,6 @@ err.steering.file-exists:
478474
short: 'steering file already exists: %s'
479475
err.steering.invalid-yaml:
480476
short: 'steering: %s: invalid YAML frontmatter: %w'
481-
err.steering.missing-closing-delimiter:
482-
short: missing closing frontmatter delimiter (---)
483-
err.steering.missing-opening-delimiter:
484-
short: missing opening frontmatter delimiter (---)
485477
err.steering.no-tool:
486478
short: 'no tool specified: use --tool <tool>, --all, or set the tool field in .ctxrc'
487479
err.steering.output-escapes-root:
@@ -572,3 +564,7 @@ err.validation.parse-file:
572564
short: 'failed to parse %s: %w'
573565
err.validation.working-directory:
574566
short: 'failed to get working directory: %w'
567+
err.parser.missing-open-delim:
568+
short: missing opening frontmatter delimiter (---)
569+
err.parser.missing-close-delim:
570+
short: missing closing frontmatter delimiter (---)

internal/config/embed/text/err_parse.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,10 @@ const (
2323
DescKeyErrParserUnmarshal = "err.parser.unmarshal"
2424
// DescKeyErrParserWalkDir is the text key for err parser walk dir messages.
2525
DescKeyErrParserWalkDir = "err.parser.walk-dir"
26+
// DescKeyErrParserMissingOpenDelim is the text key for
27+
// missing opening frontmatter delimiter.
28+
DescKeyErrParserMissingOpenDelim = "err.parser.missing-open-delim"
29+
// DescKeyErrParserMissingCloseDelim is the text key
30+
// for missing closing frontmatter delimiter.
31+
DescKeyErrParserMissingCloseDelim = "err.parser.missing-close-delim"
2632
)

internal/config/embed/text/err_skill.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,9 @@ const (
2323
DescKeyErrSkillList = "err.skill.skill-list"
2424
// DescKeyErrSkillLoad is the text key for err skill load messages.
2525
DescKeyErrSkillLoad = "err.skill.load"
26-
// DescKeyErrSkillMissingClosingDelim is the text key for err skill missing
27-
// closing delim messages.
28-
DescKeyErrSkillMissingClosingDelim = "err.skill.missing-closing-delimiter"
2926
// DescKeyErrSkillMissingName is the text key for err skill missing name
3027
// messages.
3128
DescKeyErrSkillMissingName = "err.skill.missing-name"
32-
// DescKeyErrSkillMissingOpeningDelim is the text key for err skill missing
33-
// opening delim messages.
34-
DescKeyErrSkillMissingOpeningDelim = "err.skill.missing-opening-delimiter"
3529
// DescKeyErrSkillNotFound is the text key for err skill not found messages.
3630
DescKeyErrSkillNotFound = "err.skill.not-found"
3731
// DescKeyErrSkillNotValidDir is the text key for err skill not valid dir

internal/config/embed/text/err_steering.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,6 @@ const (
2323
// DescKeyErrSteeringInvalidYAML is the text key for err steering invalid yaml
2424
// messages.
2525
DescKeyErrSteeringInvalidYAML = "err.steering.invalid-yaml"
26-
// DescKeyErrSteeringMissingClosingDelim is the text key for err steering
27-
// missing closing delim messages.
28-
DescKeyErrSteeringMissingClosingDelim = "err.steering.missing-closing-delimiter"
29-
// DescKeyErrSteeringMissingOpeningDelim is the text key for err steering
30-
// missing opening delim messages.
31-
DescKeyErrSteeringMissingOpeningDelim = "err.steering.missing-opening-delimiter"
3226
// DescKeyErrSteeringNoTool is the text key for err steering no tool messages.
3327
DescKeyErrSteeringNoTool = "err.steering.no-tool"
3428
// DescKeyErrSteeringOutputEscapesRoot is the text key for err steering output

internal/err/parser/parser.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,39 @@
77
package parser
88

99
import (
10+
"errors"
1011
"fmt"
1112

1213
"github.com/ActiveMemory/ctx/internal/assets/read/desc"
1314
"github.com/ActiveMemory/ctx/internal/config/embed/text"
1415
)
1516

17+
// MissingOpenDelim returns an error for a missing
18+
// opening frontmatter delimiter (---).
19+
//
20+
// Returns:
21+
// - error: "missing opening frontmatter delimiter"
22+
func MissingOpenDelim() error {
23+
return errors.New(
24+
desc.Text(
25+
text.DescKeyErrParserMissingOpenDelim,
26+
),
27+
)
28+
}
29+
30+
// MissingCloseDelim returns an error for a missing
31+
// closing frontmatter delimiter (---).
32+
//
33+
// Returns:
34+
// - error: "missing closing frontmatter delimiter"
35+
func MissingCloseDelim() error {
36+
return errors.New(
37+
desc.Text(
38+
text.DescKeyErrParserMissingCloseDelim,
39+
),
40+
)
41+
}
42+
1643
// ReadFile wraps a session file read failure.
1744
//
1845
// Parameters:

internal/err/skill/skill.go

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
package skill
88

99
import (
10-
"errors"
1110
"fmt"
1211

1312
"github.com/ActiveMemory/ctx/internal/assets/read/desc"
@@ -94,17 +93,6 @@ func Load(name string, cause error) error {
9493
)
9594
}
9695

97-
// MissingClosingDelimiter returns an error for missing closing
98-
// frontmatter delimiter.
99-
//
100-
// Returns:
101-
// - error: "missing closing frontmatter delimiter (---)"
102-
func MissingClosingDelimiter() error {
103-
return errors.New(
104-
desc.Text(text.DescKeyErrSkillMissingClosingDelim),
105-
)
106-
}
107-
10896
// MissingName returns an error for a skill manifest missing the
10997
// required name field.
11098
//
@@ -119,17 +107,6 @@ func MissingName(manifest string) error {
119107
)
120108
}
121109

122-
// MissingOpeningDelimiter returns an error for missing opening
123-
// frontmatter delimiter.
124-
//
125-
// Returns:
126-
// - error: "missing opening frontmatter delimiter (---)"
127-
func MissingOpeningDelimiter() error {
128-
return errors.New(
129-
desc.Text(text.DescKeyErrSkillMissingOpeningDelim),
130-
)
131-
}
132-
133110
// NotFound returns an error when a skill cannot be found by name.
134111
//
135112
// Parameters:

internal/err/steering/steering.go

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -78,28 +78,6 @@ func InvalidYAML(filePath string, cause error) error {
7878
)
7979
}
8080

81-
// MissingClosingDelimiter returns an error for missing closing
82-
// frontmatter delimiter.
83-
//
84-
// Returns:
85-
// - error: "missing closing frontmatter delimiter (---)"
86-
func MissingClosingDelimiter() error {
87-
return errors.New(
88-
desc.Text(text.DescKeyErrSteeringMissingClosingDelim),
89-
)
90-
}
91-
92-
// MissingOpeningDelimiter returns an error for missing opening
93-
// frontmatter delimiter.
94-
//
95-
// Returns:
96-
// - error: "missing opening frontmatter delimiter (---)"
97-
func MissingOpeningDelimiter() error {
98-
return errors.New(
99-
desc.Text(text.DescKeyErrSteeringMissingOpeningDelim),
100-
)
101-
}
102-
10381
// NoTool returns an error when no tool is specified for sync.
10482
//
10583
// Returns:

internal/parse/markdown.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/ActiveMemory/ctx/internal/config/regex"
1313
"github.com/ActiveMemory/ctx/internal/config/token"
14+
errParser "github.com/ActiveMemory/ctx/internal/err/parser"
1415
)
1516

1617
// StripLineNumbers removes Claude Code's line number prefixes from content.
@@ -85,3 +86,48 @@ func FenceForContent(content string) string {
8586
}
8687
return fence
8788
}
89+
90+
// SplitFrontmatter separates YAML frontmatter from a
91+
// markdown body. Frontmatter must start with a ---
92+
// line and end with a second --- line.
93+
//
94+
// Parameters:
95+
// - data: Raw file bytes
96+
//
97+
// Returns:
98+
// - []byte: YAML frontmatter (between delimiters)
99+
// - string: Body after the closing delimiter
100+
// - error: Non-nil if delimiters are missing
101+
func SplitFrontmatter(
102+
data []byte,
103+
) ([]byte, string, error) {
104+
content := strings.TrimLeft(
105+
string(data), token.TrimCR,
106+
)
107+
108+
if !strings.HasPrefix(
109+
content, token.FrontmatterDelimiter,
110+
) {
111+
return nil, "", errParser.MissingOpenDelim()
112+
}
113+
114+
rest := content[len(token.FrontmatterDelimiter):]
115+
rest = strings.TrimPrefix(rest, token.NewlineLF)
116+
117+
needle := token.NewlineLF +
118+
token.FrontmatterDelimiter
119+
idx := strings.Index(rest, needle)
120+
if idx < 0 {
121+
return nil, "", errParser.MissingCloseDelim()
122+
}
123+
124+
fm := rest[:idx]
125+
after := rest[idx+1+len(
126+
token.FrontmatterDelimiter,
127+
):]
128+
after = strings.TrimPrefix(
129+
after, token.NewlineLF,
130+
)
131+
132+
return []byte(fm), after, nil
133+
}

internal/skill/manifest.go

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,59 +7,34 @@
77
package skill
88

99
import (
10-
"strings"
11-
1210
"gopkg.in/yaml.v3"
1311

14-
"github.com/ActiveMemory/ctx/internal/config/token"
1512
errSkill "github.com/ActiveMemory/ctx/internal/err/skill"
13+
"github.com/ActiveMemory/ctx/internal/parse"
1614
)
1715

18-
// parseManifest extracts YAML frontmatter and markdown body from a
19-
// SKILL.md file. The frontmatter is delimited by --- lines.
20-
func parseManifest(data []byte, name, dir string) (*Skill, error) {
21-
raw, body, splitErr := splitFrontmatter(data)
16+
// parseManifest extracts YAML frontmatter and markdown
17+
// body from a SKILL.md file.
18+
func parseManifest(
19+
data []byte, name, dir string,
20+
) (*Skill, error) {
21+
raw, body, splitErr := parse.SplitFrontmatter(
22+
data,
23+
)
2224
if splitErr != nil {
2325
return nil, errSkill.Load(name, splitErr)
2426
}
2527

2628
sk := &Skill{}
27-
if unmarshalErr := yaml.Unmarshal(raw, sk); unmarshalErr != nil {
28-
return nil, errSkill.InvalidYAML(name, unmarshalErr)
29+
if unmarshalErr := yaml.Unmarshal(
30+
raw, sk,
31+
); unmarshalErr != nil {
32+
return nil, errSkill.InvalidYAML(
33+
name, unmarshalErr,
34+
)
2935
}
3036

3137
sk.Body = body
3238
sk.Dir = dir
3339
return sk, nil
3440
}
35-
36-
// splitFrontmatter separates YAML frontmatter from the markdown body.
37-
// Frontmatter must start with a --- line and end with a second --- line.
38-
func splitFrontmatter(
39-
data []byte,
40-
) (frontmatter []byte, body string, err error) {
41-
content := strings.TrimLeft(string(data), token.TrimCR)
42-
43-
if !strings.HasPrefix(content, token.FrontmatterDelimiter) {
44-
return nil, "", errSkill.MissingOpeningDelimiter()
45-
}
46-
47-
// Skip the opening delimiter line.
48-
rest := content[len(token.FrontmatterDelimiter):]
49-
rest = strings.TrimPrefix(rest, token.NewlineLF)
50-
51-
needle := token.NewlineLF + token.FrontmatterDelimiter
52-
idx := strings.Index(rest, needle)
53-
if idx < 0 {
54-
return nil, "", errSkill.MissingClosingDelimiter()
55-
}
56-
57-
fm := rest[:idx]
58-
59-
// Skip past the closing delimiter line.
60-
after := rest[idx+1+len(token.FrontmatterDelimiter):]
61-
// Trim exactly one leading newline from the body if present.
62-
after = strings.TrimPrefix(after, token.NewlineLF)
63-
64-
return []byte(fm), after, nil
65-
}

internal/steering/frontmatter.go

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,9 @@
77
package steering
88

99
import (
10-
"strings"
11-
1210
cfgSteering "github.com/ActiveMemory/ctx/internal/config/steering"
13-
"github.com/ActiveMemory/ctx/internal/config/token"
14-
errSteering "github.com/ActiveMemory/ctx/internal/err/steering"
1511
)
1612

17-
// splitFrontmatter separates YAML frontmatter from the markdown body.
18-
// Frontmatter must start with a --- line and end with a second --- line.
19-
func splitFrontmatter(
20-
data []byte,
21-
) (frontmatter []byte, body string, err error) {
22-
content := string(data)
23-
content = strings.TrimLeft(content, token.TrimCR)
24-
25-
if !strings.HasPrefix(content, token.FrontmatterDelimiter) {
26-
return nil, "", errSteering.MissingOpeningDelimiter()
27-
}
28-
29-
// Skip the opening delimiter line.
30-
rest := content[len(token.FrontmatterDelimiter):]
31-
rest = strings.TrimPrefix(rest, token.NewlineLF)
32-
33-
needle := token.NewlineLF + token.FrontmatterDelimiter
34-
idx := strings.Index(rest, needle)
35-
if idx < 0 {
36-
return nil, "", errSteering.MissingClosingDelimiter()
37-
}
38-
39-
fm := rest[:idx]
40-
41-
// Skip past the closing delimiter line.
42-
after := rest[idx+1+len(token.FrontmatterDelimiter):]
43-
// Trim exactly one leading newline from the body if present.
44-
after = strings.TrimPrefix(after, token.NewlineLF)
45-
46-
return []byte(fm), after, nil
47-
}
48-
4913
// applyDefaults sets default values for fields not present in the
5014
// parsed frontmatter.
5115
func applyDefaults(sf *SteeringFile) {

0 commit comments

Comments
 (0)