1pub const AGENT_RULE: &str = include_str!("../skills/opys/agent-rule.md");
7
8pub const CLAUDE_MD_SNIPPET: &str = r#"## Feature inventory
9
10- opys manages a file-based inventory of typed documents under `opys/`,
11 one markdown file per document; the document *types* (their ID prefixes,
12 statuses, fields, required sections, and validation rules) are configured in
13 `opys.toml`.
14- To find documents: `rg` by tag/status or `opys list`, then read only the
15 relevant files. Do not bulk-read the inventory.
16- To create or change documents, use `opys` (`new --type`, set-status, tag,
17 retire, block, close). Body prose and section edits are normal file edits —
18 run `opys verify` before finishing.
19- When implementing a feature: read its file fully; implement; add tests; check
20 the matching test-plan items and append backticked test references; set status
21 via the CLI. If a test plan's case enumeration looks incomplete, raise it.
22- Track ephemeral implementation work in work-item-style types (e.g.
23 `opys new --type bug --features FEAT-0001`), and `opys close` them when done.
24 Never record test results, dates, or completion claims in documents.
25"#;
26
27pub const DEFAULT_OPYS_CONFIG: &str = r##"# opys.toml — the opys document-inventory config. Lives at the project root;
30# opys finds it by searching upward from the current directory.
31
32base = "opys" # inventory dir (relative to this file)
33pad = 4 # zero-padding width for the numeric id part
34
35# On-disk layout. Each document's path (under `base`) is this template with
36# {type} → the type's `dir`, {status} → the type's `status_dirs[status]`, and
37# {id} → PREFIX-NNNN. Both segments are empty by default, so documents live flat
38# at the base (e.g. opys/FEAT-0001.md). Empty segments collapse, so the order is
39# free — e.g. "{status}/{type}/{id}.md" groups by status first.
40[layout]
41path = "{type}/{status}/{id}.md"
42
43# How document ids are mentioned in code, for `opys show <id> --refs` and the
44# warning `opys renumber` prints when a renumbered id is still referenced. Each
45# format renders an id with the placeholders {id} (the full PREFIX-NNNN),
46# {prefix}/{prefix_lower}, {num} (unpadded), and {padded} (zero-padded to `pad`);
47# `word = false` matches a substring instead of a whole word. With no formats
48# configured, only the canonical {id} is scanned.
49[file_refs]
50roots = ["src", "tests"] # where to scan (project-root relative)
51formats = [
52 { template = "{id}" }, # FEAT-0001
53 { template = "{prefix}{num}" }, # FEAT1
54 { template = "{prefix_lower}_{num}" }, # feat_1
55]
56
57# ---------------------------------- feature ----------------------------------
58# Permanent description of product behavior. A feature removed from the product
59# becomes status "archived" (kept in the inventory), never deleted.
60[types.feature]
61prefix = "FEAT"
62statuses = ["planned", "partial", "implemented", "wontfix", "archived"]
63default_status = "planned"
64terminal_statuses = [] # features are never closed/deleted
65tags_required = true
66status_dirs = { archived = "_archived" } # archived features move to opys/_archived/
67
68[types.feature.fields.spec]
69type = "string"
70pattern = '^\S.*$' # non-empty, single line
71description = "Path to long-form shared spec material"
72
73[types.feature.fields.wontfix_reason]
74type = "string"
75
76[types.feature.fields.archived_reason]
77type = "string"
78
79[[types.feature.sections]]
80heading = "Test plan"
81kind = "checklist" # structure: checkbox items
82
83# Each checked item must carry a resolvable test reference. `pattern` parses a
84# `mod::name` backtick span; `must_match` greps the test name under `roots`.
85[[types.feature.sections.checks]]
86pattern = '`(?P<ref>[^`]*::(?P<name>[^`]+))`'
87roots = ["src", "tests"]
88must_match = '${name}' # ${group} = the regex-escaped capture
89scope = "checked" # "all" (every line) | "checked" (checked items)
90message = "test reference `${ref}` not found"
91
92# A `structured` section: its content shape is an mdprism schema (the body
93# portion of the DSL — see docs/structure-dsl-spec.md). This reproduces the
94# classic manual-QA shape with Setup / Procedure / Expectations sub-sections;
95# the format is yours to change per type.
96[[types.feature.sections]]
97heading = "Manual verification"
98kind = "structured"
99structure = '''
100### @setup Setup
101 - +@items
102### @procedure Procedure
103 1. +@steps
104### @expect Expectations
105 - +@checks
106'''
107
108# ----------------------------------- task ------------------------------------
109# Ephemeral implementation work. Deleted on `close`.
110[types.task]
111prefix = "TASK"
112statuses = ["todo", "in-progress", "blocked", "done"]
113default_status = "todo"
114terminal_statuses = ["done"] # "done" reached only via `close`
115tags_required = false
116requires_link = { to = "feature", min = 1 }
117
118[types.task.fields.blocked_reason]
119type = "string"
120
121[[types.task.sections]]
122heading = "Tasks"
123kind = "checklist"
124required = true
125
126[[types.task.sections]]
127heading = "Progress"
128kind = "log"
129required = true
130
131# ------------------------------------ bug ------------------------------------
132# Like task, plus a required Reproduction section.
133[types.bug]
134prefix = "BUG"
135statuses = ["todo", "in-progress", "blocked", "done"]
136default_status = "todo"
137terminal_statuses = ["done"]
138tags_required = false
139requires_link = { to = "feature", min = 1 }
140
141[types.bug.fields.blocked_reason]
142type = "string"
143
144[[types.bug.sections]]
145heading = "Reproduction"
146kind = "prose"
147required = true
148
149[[types.bug.sections]]
150heading = "Tasks"
151kind = "checklist"
152required = true
153
154[[types.bug.sections]]
155heading = "Progress"
156kind = "log"
157required = true
158
159# ----------------------------------- chore -----------------------------------
160# Maintenance/tooling work with no user-facing behavior change.
161[types.chore]
162prefix = "CHORE"
163statuses = ["todo", "in-progress", "blocked", "done"]
164default_status = "todo"
165terminal_statuses = ["done"]
166tags_required = false
167requires_link = { to = "feature", min = 1 }
168
169[types.chore.fields.blocked_reason]
170type = "string"
171
172[[types.chore.sections]]
173heading = "Tasks"
174kind = "checklist"
175required = true
176
177[[types.chore.sections]]
178heading = "Progress"
179kind = "log"
180required = true
181
182# ------------------------------ validation rules -----------------------------
183# Each rule: an optional `when { type?, status? }` + one assertion. Closed set:
184# require_field, field_matches, require_section, require_checked_section,
185# require_link, require_any.
186
187[[rules]]
188when = { type = "feature", status = "wontfix" }
189require_field = "wontfix_reason"
190
191[[rules]]
192when = { type = "feature", status = "archived" }
193require_field = "archived_reason"
194
195[[rules]]
196when = { type = "feature", status = "implemented" }
197require_checked_section = "Test plan"
198
199[[rules]]
200when = { status = "blocked" }
201require_any = [{ field = "blocked_reason" }, { link = "blocked_by" }]
202
203# ---------------------------------- stats ------------------------------------
204# `opys stats` renders these sections (and the TUI stats screen shows the same
205# text). Each stat is a single SQL query over an in-memory relational view of the
206# corpus; its result set is rendered as a markdown table headed by `name`. The
207# tables (all rebuilt on every run, read-only):
208# docs(id, num, type, status, title, created, updated)
209# tags(doc_id, tag, key, value) -- one row per tag; value NULL for a plain tag
210# sections(doc_id, heading, kind, items, unchecked) -- countable sections only
211# fields(doc_id, key, value) -- one row per declared field (per element for lists)
212# Any SQL GlueSQL supports works (GROUP BY, subqueries, CASE, JOIN, …). Edit freely.
213# Add an optional `template` to render each result row through it (`{column}`
214# placeholders) instead of a table — handy for single-row summaries. Omit it for
215# the default table (these three are naturally tabular, so they use tables).
216
217[[stats]]
218name = "Status by type"
219sql = '''
220SELECT type, status, COUNT(*) AS count,
221 ROUND(100.0 * COUNT(*) /
222 (SELECT COUNT(*) FROM docs d2 WHERE d2.type = d1.type)) AS pct
223FROM docs d1
224GROUP BY type, status
225ORDER BY type, status
226'''
227
228[[stats]]
229name = "Tags by key"
230sql = '''
231SELECT key, value, COUNT(DISTINCT doc_id) AS docs
232FROM tags
233WHERE value IS NOT NULL
234GROUP BY key, value
235ORDER BY key, value
236'''
237
238[[stats]]
239name = "Section coverage"
240sql = '''
241SELECT heading, kind, SUM(items) AS items, SUM(unchecked) AS uncovered
242FROM sections
243GROUP BY heading, kind
244ORDER BY heading
245'''
246"##;
247
248#[cfg(test)]
249mod tests {
250 #[test]
253 fn default_opys_config_is_valid_toml() {
254 toml::from_str::<toml::Value>(super::DEFAULT_OPYS_CONFIG)
255 .expect("DEFAULT_OPYS_CONFIG must be valid TOML");
256 }
257
258 #[test]
262 fn default_opys_config_validates() {
263 let cfg: crate::project_config::ProjectConfig =
264 toml::from_str(super::DEFAULT_OPYS_CONFIG).expect("parses as ProjectConfig");
265 let problems = cfg.validate();
266 assert!(problems.is_empty(), "default config problems: {problems:?}");
267 }
268}