Documentation
¶
Overview ¶
Package rule parses, scaffolds, and indexes SpecScore Rule artifacts.
A Rule is the *normative* artifact kind: one sentence an agent or human MUST or MUST NEVER do, with the scope it binds, the reason it exists, the sources that produced it, and the control that enforces it. It is deliberately the smallest kind in the spec tree — a Lesson explains a process gap, a Decision explains a choice, an Idea explains a direction; a Rule is the single transferable sentence that survives all three and can be handed to a fresh agent with no other context.
Rules exist because durable operating knowledge accumulated in per-agent memory files, which do not transfer: a new session, a new machine, or a new runtime starts blind. A Rule is reviewable, greppable, scoped, and — at the Enforced and Automated tiers — attached to a named control.
Two forms, one entity ¶
An INLINE rule is exactly one row in spec/rules/README.md and nothing else. Most rules are one sentence; giving each of them a directory would be ceremony that discourages recording them at all.
A DETAILED rule keeps the identical index row and adds spec/rules/<slug>/README.md carrying the reason, worked compliant and violating examples, agent instructions, exceptions, and supersession. Its index row's identity cell is a link, so the index alone says which rules have more to read.
The index row is the source of truth for every field it carries. A detail document repeats those fields in its header for readability, and lint (R-011) requires them to agree — with --fix rewriting the document from the row, never the reverse. That is what keeps two representations of one rule from drifting into two different rules.
Index ¶
- Constants
- Variables
- func ApplyFieldEdits(content []byte, edits []FieldEdit) ([]byte, error)
- func ClearLessonPromotesTo(lessonPath string) error
- func DetailPath(rulesDir, slug string) string
- func DetailsBySlug(rulesDir string) (map[string]*Detail, error)
- func EnforcementList() string
- func EnsureIndex(rulesDir string) error
- func IndexContent() string
- func IndexPath(rulesDir string) string
- func IsEnforcement(value string) bool
- func IsStatus(value string) bool
- func LessonSources(sources []SourceRef) []string
- func MergeSources(current, add, remove []string) ([]string, error)
- func MirroredValuesOf(row Row) map[string]string
- func ParseEnforcement(value string) (string, bool)
- func ParsePromotesTo(value string) (slug string, ok bool, err error)
- func ParseStatus(value string) (string, bool)
- func PromotesToRef(slug string) string
- func RemoveRow(rulesDir, slug string) error
- func RepairHint(m MalformedRow) string
- func RequiresControl(tier string) bool
- func ResolveTrigger(statement string, detail *Detail) string
- func RulesDir(projectRoot string) string
- func ScaffoldDetail(opts Options) ([]byte, error)
- func ScopesMatch(scopes []Scope, p string) bool
- func SetLessonPromotesTo(lessonPath, ruleSlug string) error
- func SetSkillRules(skillPath string, slugs []string) error
- func SkillsByName(skillsDir string) (map[string]*Skill, error)
- func SkillsDir(projectRoot, configuredPath string) string
- func StatusList() string
- func TitleCaseFromSlug(slug string) string
- func UpsertRow(rulesDir string, row Row) error
- func ValidateSlug(slug string) error
- func WriteFileAtomic(path string, data []byte) error
- func WriteIndexRows(path string, rows []Row, preserved []MalformedRow) error
- type Detail
- func (d *Detail) HasControl() bool
- func (d *Detail) HasSection(title string) bool
- func (d *Detail) MirroredValues() map[string]string
- func (d *Detail) MissingExampleSubsections() []string
- func (d *Detail) MissingSections() []string
- func (d *Detail) Scopes() ([]Scope, error)
- func (d *Detail) Sources() ([]SourceRef, error)
- type FieldEdit
- type IndexReport
- type MalformedRow
- type Options
- type RenderEntry
- type Row
- type Scope
- type Skill
- type SourceRef
Constants ¶
const ( IndexHeaderRow = "| Rule | Status | Scope | Enforcement | Control | Sources | Statement |" IndexSeparatorRow = "|---|---|---|---|---|---|---|" )
IndexHeaderRow and IndexSeparatorRow are the exact canonical table header lines, compared byte-for-byte so a hand-edited index that dropped a column is reported rather than silently reinterpreted.
const ( ScopeFleet = "fleet" // every repository, every product. ScopeProduct = "product" // one product by name, e.g. product:sneat. ScopeRepo = "repo" // one repository, e.g. repo:specscore/specscore-cli. ScopePath = "path" // a glob over paths, e.g. path:**/*.go. )
Scope kinds. A Rule binds everywhere its Scope list says it binds, and nowhere else — the property that lets `rule list --applies-to <path>` answer "which rules apply to what I am about to touch" without a human reading them all.
const ( SourceLesson = "lesson" SourceDecision = "decision" SourceIdea = "idea" SourceURL = "url" // a free http(s) reference; never resolved locally )
Source kinds. A Rule's **Sources:** list names the artifacts that produced it, so a reader can always get back to *why* — the Lesson whose process gap it closes, the Decision that chose it, the Idea that proposed it — without the Rule itself having to restate any of them.
const DefaultSkillsPath = "ai/skills"
DefaultSkillsPath is the repository-relative directory scanned for skills when specscore.yaml declares no override.
const FormatURL = "https://specscore.md/rule-specification"
FormatURL is the canonical spec URL for the Rule detail document. It is carried verbatim in both the frontmatter `format:` field and the adherence-footer line, per the artifact-frontmatter-convention.
const IndexEmptyPlaceholder = "_No rules recorded yet._"
IndexEmptyPlaceholder is written in place of the table body when a project has no Rules yet.
const IndexFormatURL = "https://specscore.md/rules-index-specification"
IndexFormatURL is the canonical spec URL for the rules index (spec/rules/README.md) — which, unusually for an index, is also the primary artifact: an inline rule lives nowhere else.
const IndexHeading = "## Rules"
IndexHeading is the H2 the rules table lives under.
const LessonPromotesToField = "Promotes To"
LessonPromotesToField is the bold field name written on the Lesson.
const Sentinel = "—"
Sentinel is the em-dash placeholder every optional field carries when it has no value. An empty value is a violation; the sentinel is the explicit "nothing here" a reader can trust (mirroring the canonical Lesson's relation fields).
const SkillRulesHeading = "## Rules"
SkillRulesHeading is the H2 a skill uses to declare the rules that bind it.
const TriggerMaxLen = 90
TriggerMaxLen is the character limit **Trigger:** must stay under — a progressive-discovery line, not a restatement of the rule, so it has to fit on one skimmable line the way a skill's own trigger description does.
Variables ¶
var DetailFields = []string{
"Status",
"Date",
"Owner",
"Statement",
"Scope",
"Enforcement",
"Control",
"Sources",
"Why",
"Exceptions",
"Supersedes",
"Superseded By",
}
DetailFields is the closed, ordered set of bold metadata fields every detail document MUST declare. The mirrored block comes first, then the fields the document alone owns.
var DetailSections = []string{"Instructions", "Examples", "Open Questions"}
DetailSections is the closed, ordered set of H2 headings a detail document MUST carry. Instructions is what an agent acts on; Examples is what stops "never mock a backend" from being read three different ways.
var EnforcementTiers = []string{"Stated", "Enforced", "Automated"}
EnforcementTiers is the closed, ordered **Enforcement:** vocabulary.
- Stated — an agent or human is told; nothing refuses.
- Enforced — a named control refuses (a wb verb, a hook profile, a CI check, a review probe).
- Automated — a named control both refuses and repairs, with no human in the loop.
Enforced and Automated MUST name a control; Stated MUST NOT be required to, because the whole point of that tier is that no control exists yet.
var ExampleSubsections = []string{"Compliant", "Violation"}
ExampleSubsections are the H3 headings required inside `## Examples`. Both are required: a rule with only a compliant example teaches the happy path and leaves the reader guessing at the boundary the rule actually draws.
var MirroredFields = []string{"Status", "Statement", "Scope", "Enforcement", "Control", "Sources"}
MirroredFields are the header fields a detail document repeats from its index row, in canonical order. The row is authoritative for every one of them.
var Statuses = []string{"Draft", "Active", "Superseded"}
Statuses is the closed, ordered Rule **Status:** vocabulary.
- Draft — written down, not yet binding.
- Active — binding within its declared Scope.
- Superseded — replaced by another Rule, named in **Superseded By:**.
Functions ¶
func ApplyFieldEdits ¶
ApplyFieldEdits rewrites the named bold fields of a rule detail document in place, preserving every other byte — comments, worked examples, hand-written Open Questions, section order, trailing whitespace. Nothing is regenerated from a template, because an update that reformatted the whole document would silently discard an author's examples and make `rule update` unsafe to run on a reviewed rule.
When **Status:** is edited, the frontmatter `status:` mirror is rewritten with it, so the status-mirror lint rule can never be broken by an update.
A named field absent from the document is inserted directly after the last present field that precedes it in DetailFields order; that keeps the canonical ordering intact for a document written before the field existed.
func ClearLessonPromotesTo ¶
ClearLessonPromotesTo removes the Lesson's `**Promotes To:**` field entirely. Writing the em-dash sentinel instead would leave a dangling "promoted to nothing" line that reads as a half-finished promotion.
func DetailPath ¶
DetailPath returns the detail-document path for a slug.
func DetailsBySlug ¶
DetailsBySlug is DiscoverDetails keyed by slug.
func EnforcementList ¶
func EnforcementList() string
EnforcementList renders the canonical tier vocabulary for an error message.
func EnsureIndex ¶
EnsureIndex writes the lint-clean index stub when spec/rules/README.md does not exist. An existing file is left byte-for-byte untouched.
func IndexContent ¶
func IndexContent() string
IndexContent is the lint-clean stub written when spec/rules/README.md does not exist yet.
func IsEnforcement ¶
IsEnforcement reports whether value is one of the canonical tiers.
func LessonSources ¶
LessonSources returns just the lesson slugs a source list names, in order.
func MergeSources ¶
MergeSources applies --add-source / --remove-source to an existing list, preserving order, rejecting duplicates and unknown removals. Both inputs are validated as source references first, so a typo can never silently no-op.
func MirroredValuesOf ¶
MirroredValuesOf projects a row into the same shape, for comparison.
func ParseEnforcement ¶
ParseEnforcement resolves a case-insensitive tier name to its canonical spelling.
func ParsePromotesTo ¶
ParsePromotesTo extracts the rule slug from a Lesson's `**Promotes To:**` value. It returns ok=false for the empty value or the em-dash sentinel, and an error for a value that is present but not a `rule:<slug>` reference.
func ParseStatus ¶
ParseStatus resolves a case-insensitive status name to its canonical spelling.
func PromotesToRef ¶
PromotesToRef is the value a Lesson carries in its optional `**Promotes To:**` field when it has been promoted into a Rule.
func RemoveRow ¶
RemoveRow deletes slug's row, restoring the empty placeholder when the table becomes empty. A missing row or a missing index file is a no-op, not an error: `rule delete` must still finish on a tree whose index had drifted.
func RepairHint ¶ added in v0.39.1
func RepairHint(m MalformedRow) string
RepairHint names the repair that actually applies to a malformed row.
`--fix` can only escape a surplus `|` in a Statement, so telling a reader to run it on a row it cannot touch sends them in a circle — which is exactly what an empty identity cell used to do: every verb refused, and the refusal recommended a fixer that would not help.
func RequiresControl ¶
RequiresControl reports whether an enforcement tier obliges the Rule to name a control. Stated is exactly the tier that does not.
func ResolveTrigger ¶ added in v0.48.0
ResolveTrigger implements the fallback chain a rendered entry's trigger text follows, in order:
- The detail document's own **Trigger:** header line, when present.
- The first line of its `## Instructions` section, when that line is written `Trigger: <text>` — the shape `sneat-co/backstage`'s rule tree already uses, so a document authored there needs no edit to render.
- The row's Statement, cut at its first comma, semicolon, or colon — the clause that names the situation, before the sentence gets normative.
detail is nil for an inline rule, which has no header or Instructions to fall back to and renders from its Statement alone.
func RulesDir ¶
RulesDir returns the rules directory for a project root (the directory that contains spec/, not spec/ itself).
func ScaffoldDetail ¶
ScaffoldDetail returns a lint-clean rule detail document: the artifact-frontmatter-convention frontmatter, the `# Rule:` title, the twelve ordered bold fields (the first six mirroring the index row), Instructions, paired Examples, Open Questions, and the adherence footer.
func ScopesMatch ¶
ScopesMatch reports whether any scope in the list covers p.
func SetLessonPromotesTo ¶
SetLessonPromotesTo writes (or rewrites) the Lesson's `**Promotes To:**` field to point at ruleSlug. It preserves every other byte of the Lesson.
func SetSkillRules ¶
SetSkillRules rewrites a skill's `## Rules` section to list exactly slugs, appending the section when absent. It preserves every other byte of the skill, because a skill file is hand-written instruction text that a rule verb has no business reformatting.
func SkillsByName ¶
SkillsByName is DiscoverSkills keyed by skill name.
func SkillsDir ¶
SkillsDir resolves the skills directory for a project root, honouring the optional `rules.skills_path` override.
func StatusList ¶
func StatusList() string
StatusList renders the canonical status vocabulary for an error message.
func TitleCaseFromSlug ¶
TitleCaseFromSlug turns "never-mock-backends" into "Never Mock Backends".
func UpsertRow ¶
UpsertRow inserts or replaces exactly the row owned by row.Slug, leaving every other row byte-identical. It is deliberately narrower than a full rewrite so a create or update verb has a bounded declared write set.
func ValidateSlug ¶
ValidateSlug returns nil when slug is a lowercase, hyphen-separated, URL-safe identifier with no `/`.
func WriteFileAtomic ¶
WriteFileAtomic publishes data to path through a same-directory temp file and a rename, then fsyncs the directory, so a crash mid-write can never leave a half-written index behind.
func WriteIndexRows ¶
func WriteIndexRows(path string, rows []Row, preserved []MalformedRow) error
WriteIndexRows replaces the `## Rules` table with rows, preserving the prologue before the heading and everything from the next H2 onward (typically `## Open Questions`). Row data is never derived from anything but the rows handed in: the index is the source of truth, so a regeneration that re-read the detail documents could overwrite an author's edit with a stale mirror.
preserved holds row-like lines the contract could not parse. They are re-emitted verbatim, after the sorted rows, so that no write path in this package can reduce the rule set. A caller that drops them is deleting a rule it never managed to read — which is exactly the failure this signature exists to make impossible to write by accident.
Types ¶
type Detail ¶
type Detail struct {
Path string
Slug string
HasRuleTitle bool
TitleLine int
Title string
Status string
StatusLine int
Date string
DateLine int
Owner string
OwnerLine int
Statement string
StatementLine int
// Trigger is the optional **Trigger:** header line: the situation that
// identifies this rule's known problem, in one clause under 90 characters —
// the progressive-discovery line `rule render` prints instead of the whole
// Statement. It is not one of DetailFields: an existing document with no
// Trigger stays lint-clean, and `rule render` falls back to the
// `## Instructions` section's `Trigger: ` line, then to the Statement's
// first clause.
Trigger string
TriggerLine int
// Section is the optional **Section:** header line `rule render --format
// md` groups by (an ungrouped rule renders under `## Other`). Like Trigger,
// it is not one of DetailFields and an existing document with no Section
// stays lint-clean.
Section string
SectionLine int
// InstructionsFirstLine is the first non-blank line of the `##
// Instructions` body, used only as the Trigger fallback when it is written
// `Trigger: <text>` — the shape `sneat-co/backstage`'s rule tree already
// uses.
InstructionsFirstLine string
// ScopesRaw holds every value written on a **Scope:** line, comma-split, in
// source order. Both repeated lines and one comma-separated line parse the
// same; the CLI always writes one line.
ScopesRaw []string
ScopeText string
ScopeLine int
Enforcement string
EnforcementLine int
Control string
ControlLine int
SourcesRaw []string
SourcesText string
SourcesLine int
Why string
WhyLine int
Exceptions string
ExceptionsLine int
Supersedes string
SupersedesLine int
SupersededBy string
SupersededByLine int
FrontmatterStatus string
FrontmatterStatusLine int
// FieldCounts records how many times each bold field name appeared, so a
// duplicated field is reportable rather than a silent last-one-wins.
FieldCounts map[string]int
// FieldOrder lists the canonical fields in the order they were found.
FieldOrder []string
SectionLines map[string]int
SubsectionLines map[string]int
// SkillRefs are the `skill:<name>` references the document's body carries,
// deduplicated and sorted. They are the rule half of the rule<->skill pair.
SkillRefs []string
}
Detail is a parsed rule detail document at spec/rules/<slug>/README.md.
Every `<Field>Line` is the 1-based source line of that bold field, or 0 when the field is absent — the same shape pkg/lesson uses, so a lint violation can point at a line rather than a whole file.
func DiscoverDetails ¶
DiscoverDetails parses every rule detail document under rulesDir, sorted by slug. A missing directory is not an error: it yields an empty set, so every read verb works in a repository that has recorded no rule yet.
func ParseDetail ¶
ParseDetail reads a candidate rule detail document. It returns a populated Detail even when the file is not actually one (HasRuleTitle == false), so callers can tell "not a rule document" from "malformed rule document".
func (*Detail) HasControl ¶
HasControl reports whether the document names a real control.
func (*Detail) HasSection ¶
HasSection reports whether title is present as an H2 heading.
func (*Detail) MirroredValues ¶
MirroredValues projects the document's copy of the row-owned fields, so the mirror check compares like with like.
func (*Detail) MissingExampleSubsections ¶
MissingExampleSubsections returns the required H3 headings absent from `## Examples`.
func (*Detail) MissingSections ¶
MissingSections returns the required H2 headings absent from the body.
type FieldEdit ¶
FieldEdit is one in-place bold-field rewrite of a detail document. Value is written verbatim after `**<Name>:** `, so callers normalize it first.
type IndexReport ¶
type IndexReport struct {
Rows []Row
// HeaderSeen is true when the canonical header + separator pair was found.
HeaderSeen bool
// Malformed carries every row-like line the contract cannot represent,
// verbatim, so no writer has to guess at what it would be discarding.
Malformed []MalformedRow
// Duplicates lists slugs that appear in more than one row.
Duplicates []string
}
IndexReport is everything a reader or a linter needs about the index file in one pass.
func ReadIndex ¶
func ReadIndex(path string) (IndexReport, error)
ReadIndex scans the canonical `## Rules` table.
func (IndexReport) BySlug ¶
func (rep IndexReport) BySlug() map[string]Row
BySlug indexes the report's rows.
func (IndexReport) HasMalformed ¶
func (rep IndexReport) HasMalformed() bool
HasMalformed reports whether the index carries unparseable row-like content.
func (IndexReport) MalformedExcept ¶
func (rep IndexReport) MalformedExcept(slugs ...string) []MalformedRow
MalformedExcept returns the malformed rows that are NOT one of the named slugs. A verb editing `x` may proceed over a broken row whose identity cell reads `x` — it is about to replace that row — but must not touch the index while some other rule's row is broken.
func (IndexReport) MalformedLines ¶
func (rep IndexReport) MalformedLines() []int
MalformedLines returns the 1-based source lines of the unparseable content.
func (IndexReport) Slugs ¶
func (rep IndexReport) Slugs() []string
Slugs returns every row slug, sorted and deduplicated.
type MalformedRow ¶
type MalformedRow struct {
// Line is the 1-based source line in the index.
Line int
// Text is the line exactly as written, including its leading pipe.
Text string
// SlugHint is the identity cell's slug when that much parsed, so a caller
// can tell "the row I am editing is the broken one" from "I am about to
// step on someone else's broken row".
SlugHint string
// Reason explains, in one clause, why the line did not parse.
Reason string
}
MalformedRow is a row-like line the seven-column contract cannot represent.
Text is kept verbatim, and every writer in this package re-emits it unchanged. That is the whole point: the most ordinary authoring slip there is — an unescaped `|` pasted into a Statement — must never cause the rule to disappear. A kind that exists so operating knowledge stops evaporating cannot have a path where a benign command evaporates one.
func (MalformedRow) Repair ¶
func (m MalformedRow) Repair() (Row, bool)
Repair attempts the one repair that is unambiguous: a row with SURPLUS cells because the Statement — the last column, and the only free-text one after Sources — carried an unescaped `|`. The surplus cells are rejoined with their pipe restored and then escaped.
It is only safe because the four columns between the identity cell and the Statement have constrained grammars. All four must validate before the tail is rejoined, which is what rules out the other reading — a pipe inside Control, where the same line shape would otherwise be silently reinterpreted. Anything else is reported rather than guessed at.
type Options ¶
type Options struct {
Slug string
Title string // detail only; defaults to a title-cased slug
Owner string // detail only; defaults to "unknown"
Date string // detail only; ISO-8601, defaults to today's UTC date
Status string // defaults to Draft
Statement string // defaults to a TODO prompt
Trigger string // detail only; optional, the progressive-discovery trigger line
Scopes []string // defaults to ["fleet"]
Enforcement string // defaults to Stated
Control string // defaults to the em-dash sentinel
Sources []string // defaults to the em-dash sentinel
// Detail-only fields.
Why string // defaults to a TODO prompt
Exceptions string // defaults to "none"
Supersedes string // defaults to the em-dash sentinel
Instructions string // defaults to a TODO prompt
Compliant string // a worked compliant example
Violation string // a worked violating example
Skills []string // `skill:<name>` references to record in Instructions
}
Options carries everything both forms of a rule need. Only Slug is mandatory: a rule recorded under time pressure with nothing but a slug is still a lint-clean inline row that a later `rule update` can sharpen, which is the whole reason the kind exists.
type RenderEntry ¶ added in v0.48.0
RenderEntry is one line of a `rule render` progressive-discovery listing: the trigger phrase that identifies the known problem, the rule it points to, and the optional Section it groups under.
func BuildRenderEntries ¶ added in v0.48.0
func BuildRenderEntries(rows []Row, details map[string]*Detail) []RenderEntry
BuildRenderEntries resolves each row's trigger and section, sorted stably by Section then Trigger then Slug — a rule with no Section sorts by Trigger alone once every entry with one is interleaved by the empty-string key, so a tree where no rule carries a Section reduces to a plain trigger sort. rows is expected to already be filtered to the rules the caller wants rendered (status and scope); details maps a slug to its parsed detail document, absent for an inline rule.
func (RenderEntry) RenderLine ¶ added in v0.48.0
func (e RenderEntry) RenderLine() string
RenderLine renders one entry in the canonical `- <trigger> → rule:<slug>` shape, shared by the text and Markdown writers.
type Row ¶
type Row struct {
Slug string
// Linked is true when the identity cell is a Markdown link, which is the
// index's own statement that spec/rules/<slug>/README.md exists.
Linked bool
Status string
Scope string // raw, comma-separated
Enforcement string
Control string
Sources string // raw, comma-separated
Statement string
// Line is the 1-based source line of the row, for violation reporting.
Line int
}
Row is one rule as the index records it — the whole of an inline rule, and the authoritative half of a detailed one.
func NewRow ¶
func NewRow(slug string, linked bool, status, statement string, scopes []string, enforcement, control string, sources []string) Row
NewRow builds a canonical row from already-normalized values.
func ResolveRow ¶
ResolveRow returns the index row for slug, or an exit-3 NotFound error naming it. The index is the source of truth, so this — not a directory probe — is how every verb resolves a rule.
func RowFromDetail ¶
RowFromDetail projects a detail document back into its canonical row. It is used only to repair an index that lost a row for an existing document; the row remains authoritative everywhere else.
func (Row) DetailLink ¶
DetailLink is the identity cell's link target for a detailed rule.
func (Row) HasControl ¶
HasControl reports whether the row names a real control.
func (Row) SourceList ¶
SourceList parses the row's sources cell.
type Scope ¶
type Scope struct {
Kind string // one of the Scope* constants
Value string // "" for fleet; the product name, owner/repo, or glob otherwise
Raw string // the value exactly as written in the artifact
}
Scope is one parsed entry of a Rule's **Scope:** list.
func ParseScope ¶
ParseScope parses one raw scope token. It never guesses: an unprefixed token other than the bare `fleet` keyword is an error rather than a silently accepted product name, because a mis-scoped Rule is worse than a rejected one — it binds work it was never meant to bind.
func ParseScopes ¶
ParseScopes parses every entry of a scope list, reporting the first error.
func (Scope) Matches ¶
Matches reports whether this scope covers the given path.
fleet matches everything.
path:<glob> doublestar match against the slash-normalized path and
against every trailing suffix of it, so a repo-relative
pattern still matches an absolute path a caller holds.
Deliberately generous: `path:cli/**` also matches
`vendor/x/cli/y.go`.
product:<name> the name appears as a whole path segment.
repo:<owner>/<n> `<owner>` and `<n>` appear as CONSECUTIVE whole path
segments. A bare `<n>` never matches.
The repo rule is the strict one on purpose. Matching a bare repository name would make every rule scoped to a repo called `docs`, `api`, `web` or `cli` bind every path in the fleet that happens to contain that directory — and a mis-scoped rule that binds work it was never meant to bind is worse than one that fails to match, because nobody goes looking for it.
Callers that know the repository or product for certain should filter on `--scope` rather than inferring it from a path.
type Skill ¶
type Skill struct {
Name string // the containing directory name, which is also the skill name
Path string // absolute path to SKILL.md
// RuleRefs are the `rule:<slug>` references listed under `## Rules`,
// deduplicated and sorted.
RuleRefs []string
// RulesHeadingLine is the 1-based line of `## Rules`, or 0 when absent.
RulesHeadingLine int
}
Skill is a discovered agent skill and the rule slugs it declares.
func DiscoverSkills ¶
DiscoverSkills reads every <skillsDir>/<name>/SKILL.md. A missing directory is not an error: a repository with no skills simply has no pairs to check.
type SourceRef ¶
type SourceRef struct {
Kind string // one of the Source* constants
Value string // the slug, decision id, or full URL
Raw string // the entry exactly as written
}
SourceRef is one parsed entry of a Rule's **Sources:** list.
func ParseSource ¶
ParseSource parses one raw source token. An `http://` or `https://` prefix is a free URL; everything else MUST carry an explicit `<kind>:` prefix, so a bare word is rejected rather than silently filed under a guessed kind.
func ParseSources ¶
ParseSources parses every entry of a source list, reporting the first error.