Skip to content

Commit 9dd2249

Browse files
committed
✨ feat(cli): add --tag filtering and metadata to list-command-examples
- Add --tag flag for filtering catalog by fixture category - Add tags and summary fields to manifest entries - Enforce non-empty tags and summary at parse time - Update docs to describe new filtering and metadata
1 parent 6e9deb3 commit 9dd2249

11 files changed

Lines changed: 543 additions & 34 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ They are not MCP tools and are not the normal end-user interaction path.
8585
- `codex-mem audit-follow-imports [--target-profile all|artifacts|state|retry|health] [...]`
8686
Reports the same hygiene targets without deleting them. `--target-profile` can enable common audit target sets before you add path, age, fail-if-matched, or `--summary-only` controls.
8787
- `codex-mem list-command-examples`
88-
Prints the embedded import/follow example-manifest catalog shipped with the binary so operators and maintainers can discover the checked-in sample outputs without browsing the source tree. Add `--json` for a machine-readable catalog, `--command <name>` to filter to one command family, or `--format text|json` to keep only one example-output format.
88+
Prints the embedded import/follow example-manifest catalog shipped with the binary so operators and maintainers can discover the checked-in sample outputs without browsing the source tree. Catalog entries now carry stable `tags` plus a short summary. Add `--json` for a machine-readable catalog, `--command <name>` to filter to one command family, `--format text|json` to keep only one example-output format, or `--tag <name>` to browse one fixture category such as `audit-only` or `target-profile`.
8989
- `codex-mem migrate`
9090
Opens the configured SQLite database and applies embedded migrations.
9191
- `codex-mem serve`

docs/go/maintainer/dev-kickoff.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Current standing decisions worth preserving:
5858
- configuration loading uses `viper`
5959
- SQLite uses `modernc.org/sqlite`
6060
- `modelcontextprotocol/go-sdk` is the only MCP runtime path
61+
- do not point `GOCACHE` into the repository workspace; if a cache override is needed for local verification, use a temp directory outside the project tree
6162

6263
## Recommended Next Coding Slice
6364

docs/go/maintainer/development-tracker.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,34 @@ Immediate next tasks:
151151

152152
### 2026-03-18 Session Update
153153

154+
- Completed: Tightened command-example manifest parsing so each entry must now carry non-empty `tags` and `summary` metadata, not just the required command/name/format/path fields. Parser coverage now explicitly rejects missing metadata alongside malformed or empty tag lists, so the embedded example catalog contract is enforced at parse time instead of only by higher-level tests.
155+
- In progress: none.
156+
- Blockers: none.
157+
- Next step: keep this operator-catalog line focused on correctness and low-maintenance guarantees unless a clearly necessary usability gap shows up.
158+
159+
### 2026-03-18 Session Update
160+
161+
- Completed: Hardened the embedded command-example catalog tests so every JSON-visible example entry must carry non-empty `tags` and `summary` metadata, and tightened manifest tag parsing to trim whitespace, dedupe repeated tags, and reject empty tag slots in hand-edited `tags=` values.
162+
- In progress: none.
163+
- Blockers: none.
164+
- Next step: keep future operator-catalog changes focused on data quality and maintenance safety unless a clearly useful discovery feature is needed.
165+
166+
### 2026-03-18 Session Update
167+
168+
- Completed: Added `--tag` filtering plus embedded `tags` and `summary` metadata to `list-command-examples`. The manifest now classifies fixtures with stable labels such as `audit-only`, `target-profile`, `filtered`, `dry-run`, and input-shape tags, while also carrying a short human-readable purpose string for each example. Text and JSON output share the same command/format/tag filter pipeline, and sync/runtime coverage verifies the richer manifest format plus tag-based filtering.
169+
- In progress: none.
170+
- Blockers: none.
171+
- Next step: decide whether the current tags-plus-summary catalog is enough, or whether operators would benefit from one more discovery aid such as sorting or another filter dimension.
172+
173+
### 2026-03-18 Session Update
174+
175+
- Completed: Recorded a maintainer workflow constraint that `GOCACHE` must not point into the repository workspace. If a local cache override is needed for Go verification on this machine, it should use a temporary directory outside the project tree so the working tree does not accumulate cache artifacts.
176+
- In progress: none.
177+
- Blockers: none.
178+
- Next step: continue the current operator-facing extension work without reintroducing repo-local Go cache state.
179+
180+
### 2026-03-18 Session Update
181+
154182
- Completed: Added `--format` filtering to `list-command-examples`, including repeated and comma-separated format selectors. Text and JSON output now share the same embedded-manifest filtering path across command and format filters, runtime coverage verifies mixed command+format filtering plus argument rejection, and operator docs now show how to browse only text or JSON fixtures.
155183
- In progress: none.
156184
- Blockers: none.

docs/go/operator/import-ingestion.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,15 @@ If you only want one output format while browsing the catalog, filter it the sam
342342
codex-mem.exe list-command-examples --format json
343343
```
344344

345+
Each catalog row now also includes stable tags plus a short summary so you can tell similar fixtures apart without opening the files first.
346+
347+
If you only want one fixture category, use tags from the embedded catalog metadata:
348+
349+
```powershell
350+
codex-mem.exe list-command-examples --tag audit-only
351+
codex-mem.exe list-command-examples --tag target-profile --format json
352+
```
353+
345354
If a deliberate output change makes those fixtures drift, refresh the ingest fixtures from the repository root through the test-only maintainer helper:
346355

347356
```powershell

internal/app/command_example_manifest.go

Lines changed: 134 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"path"
66
"slices"
7+
"strconv"
78
"strings"
89

910
_ "embed"
@@ -21,13 +22,16 @@ type listCommandExamplesOptions struct {
2122
JSON bool
2223
Commands []string
2324
Formats []string
25+
Tags []string
2426
}
2527

2628
type commandExampleManifestEntry struct {
2729
Command string `json:"command"`
2830
Name string `json:"name"`
2931
RelativePath string `json:"path"`
3032
Format string `json:"format"`
33+
Tags []string `json:"tags,omitempty"`
34+
Summary string `json:"summary,omitempty"`
3135
}
3236

3337
type commandExampleManifestReport struct {
@@ -65,6 +69,16 @@ func parseListCommandExamplesOptions(args []string) (listCommandExamplesOptions,
6569
return listCommandExamplesOptions{}, err
6670
}
6771
options.Formats = appendUniqueStrings(options.Formats, values...)
72+
case "--tag":
73+
index++
74+
if index >= len(args) {
75+
return listCommandExamplesOptions{}, fmt.Errorf("list-command-examples --tag requires a value")
76+
}
77+
values, err := parseListCommandExampleTags(args[index])
78+
if err != nil {
79+
return listCommandExamplesOptions{}, err
80+
}
81+
options.Tags = appendUniqueStrings(options.Tags, values...)
6882
default:
6983
return listCommandExamplesOptions{}, fmt.Errorf("unknown list-command-examples flag %q", arg)
7084
}
@@ -84,6 +98,10 @@ func parseListCommandExampleFormats(raw string) ([]string, error) {
8498
return parseListCommandExamplesCSVFlag(raw, "--format", "format")
8599
}
86100

101+
func parseListCommandExampleTags(raw string) ([]string, error) {
102+
return parseListCommandExamplesCSVFlag(raw, "--tag", "tag")
103+
}
104+
87105
func parseListCommandExamplesCSVFlag(raw string, flag string, label string) ([]string, error) {
88106
raw = strings.TrimSpace(raw)
89107
if raw == "" {
@@ -144,20 +162,40 @@ func parseCommandExampleManifest(raw string) (commandExampleManifestReport, erro
144162
}
145163

146164
func parseCommandExampleManifestEntry(line string) (commandExampleManifestEntry, error) {
147-
fields := strings.Fields(line)
165+
fields, err := splitCommandExampleManifestFields(line)
166+
if err != nil {
167+
return commandExampleManifestEntry{}, err
168+
}
148169
entry := commandExampleManifestEntry{}
149170
for _, field := range fields {
150171
key, value, ok := strings.Cut(field, "=")
151172
if !ok {
152173
return commandExampleManifestEntry{}, fmt.Errorf("invalid command example manifest field %q", field)
153174
}
175+
if strings.HasPrefix(value, `"`) {
176+
unquoted, err := strconv.Unquote(value)
177+
if err != nil {
178+
return commandExampleManifestEntry{}, fmt.Errorf("invalid quoted command example manifest value %q: %w", value, err)
179+
}
180+
value = unquoted
181+
}
154182
switch key {
155183
case "command":
156184
entry.Command = value
157185
case "example":
158186
entry.Name = value
159187
case "format":
160188
entry.Format = value
189+
case "tags":
190+
if value != "" {
191+
tags, err := parseCommandExampleManifestTags(value)
192+
if err != nil {
193+
return commandExampleManifestEntry{}, err
194+
}
195+
entry.Tags = appendUniqueStrings(entry.Tags, tags...)
196+
}
197+
case "summary":
198+
entry.Summary = value
161199
case "path":
162200
entry.RelativePath = value
163201
default:
@@ -167,9 +205,67 @@ func parseCommandExampleManifestEntry(line string) (commandExampleManifestEntry,
167205
if entry.Command == "" || entry.Name == "" || entry.Format == "" || entry.RelativePath == "" {
168206
return commandExampleManifestEntry{}, fmt.Errorf("incomplete command example manifest entry %q", line)
169207
}
208+
if len(entry.Tags) == 0 {
209+
return commandExampleManifestEntry{}, fmt.Errorf("command example manifest entry %q is missing tags", line)
210+
}
211+
if strings.TrimSpace(entry.Summary) == "" {
212+
return commandExampleManifestEntry{}, fmt.Errorf("command example manifest entry %q is missing summary", line)
213+
}
170214
return entry, nil
171215
}
172216

217+
func parseCommandExampleManifestTags(raw string) ([]string, error) {
218+
parts := strings.Split(raw, ",")
219+
tags := make([]string, 0, len(parts))
220+
for _, part := range parts {
221+
tag := strings.TrimSpace(part)
222+
if tag == "" {
223+
return nil, fmt.Errorf("invalid command example manifest tags value %q", raw)
224+
}
225+
tags = appendUniqueStrings(tags, tag)
226+
}
227+
return tags, nil
228+
}
229+
230+
func splitCommandExampleManifestFields(line string) ([]string, error) {
231+
fields := make([]string, 0, 8)
232+
var current strings.Builder
233+
inQuotes := false
234+
escaped := false
235+
for _, r := range line {
236+
switch {
237+
case inQuotes:
238+
current.WriteRune(r)
239+
switch {
240+
case escaped:
241+
escaped = false
242+
case r == '\\':
243+
escaped = true
244+
case r == '"':
245+
inQuotes = false
246+
}
247+
case r == '"':
248+
inQuotes = true
249+
current.WriteRune(r)
250+
case r == ' ' || r == '\t':
251+
if current.Len() == 0 {
252+
continue
253+
}
254+
fields = append(fields, current.String())
255+
current.Reset()
256+
default:
257+
current.WriteRune(r)
258+
}
259+
}
260+
if inQuotes {
261+
return nil, fmt.Errorf("unterminated quoted command example manifest field in %q", line)
262+
}
263+
if current.Len() > 0 {
264+
fields = append(fields, current.String())
265+
}
266+
return fields, nil
267+
}
268+
173269
func formatCommandExampleManifestJSON(report commandExampleManifestReport) (string, error) {
174270
return marshalIndented(report)
175271
}
@@ -179,10 +275,12 @@ func formatCommandExampleManifest(report commandExampleManifestReport) string {
179275
lines = append(lines, "command example manifest "+report.Version)
180276
for _, entry := range report.Examples {
181277
lines = append(lines, fmt.Sprintf(
182-
"command=%s example=%s format=%s path=%s",
278+
"command=%s example=%s format=%s tags=%s summary=%s path=%s",
183279
entry.Command,
184280
entry.Name,
185281
entry.Format,
282+
strings.Join(entry.Tags, ","),
283+
strconv.Quote(entry.Summary),
186284
entry.RelativePath,
187285
))
188286
}
@@ -206,13 +304,15 @@ func commandExampleManifestEntriesForReport(entries []commandExampleManifestEntr
206304
Name: entry.Name,
207305
RelativePath: path.Join(commandExampleDirName, entry.RelativePath),
208306
Format: entry.Format,
307+
Tags: slices.Clone(entry.Tags),
308+
Summary: entry.Summary,
209309
})
210310
}
211311
return reportEntries
212312
}
213313

214-
func filterCommandExampleManifestReport(report commandExampleManifestReport, commands []string, formats []string) (commandExampleManifestReport, error) {
215-
if len(commands) == 0 && len(formats) == 0 {
314+
func filterCommandExampleManifestReport(report commandExampleManifestReport, commands []string, formats []string, tags []string) (commandExampleManifestReport, error) {
315+
if len(commands) == 0 && len(formats) == 0 && len(tags) == 0 {
216316
return report, nil
217317
}
218318

@@ -224,13 +324,21 @@ func filterCommandExampleManifestReport(report commandExampleManifestReport, com
224324
for _, format := range formats {
225325
allowedFormats[strings.TrimSpace(format)] = struct{}{}
226326
}
327+
allowedTags := make(map[string]struct{}, len(tags))
328+
for _, tag := range tags {
329+
allowedTags[strings.TrimSpace(tag)] = struct{}{}
330+
}
227331

228332
seenCommands := make(map[string]struct{}, len(report.Examples))
229333
seenFormats := make(map[string]struct{}, len(report.Examples))
334+
seenTags := make(map[string]struct{}, len(report.Examples))
230335
filtered := make([]commandExampleManifestEntry, 0, len(report.Examples))
231336
for _, entry := range report.Examples {
232337
seenCommands[entry.Command] = struct{}{}
233338
seenFormats[entry.Format] = struct{}{}
339+
for _, tag := range entry.Tags {
340+
seenTags[tag] = struct{}{}
341+
}
234342
if len(allowedCommands) > 0 {
235343
if _, ok := allowedCommands[entry.Command]; !ok {
236344
continue
@@ -241,6 +349,18 @@ func filterCommandExampleManifestReport(report commandExampleManifestReport, com
241349
continue
242350
}
243351
}
352+
if len(allowedTags) > 0 {
353+
matchedTag := false
354+
for _, tag := range entry.Tags {
355+
if _, ok := allowedTags[tag]; ok {
356+
matchedTag = true
357+
break
358+
}
359+
}
360+
if !matchedTag {
361+
continue
362+
}
363+
}
244364
filtered = append(filtered, entry)
245365
}
246366

@@ -264,6 +384,16 @@ func filterCommandExampleManifestReport(report commandExampleManifestReport, com
264384
return commandExampleManifestReport{}, fmt.Errorf("unknown list-command-examples format filter %q", strings.Join(unknownFormats, ","))
265385
}
266386

387+
unknownTags := make([]string, 0)
388+
for _, tag := range tags {
389+
if _, ok := seenTags[tag]; !ok && !slices.Contains(unknownTags, tag) {
390+
unknownTags = append(unknownTags, tag)
391+
}
392+
}
393+
if len(unknownTags) > 0 {
394+
return commandExampleManifestReport{}, fmt.Errorf("unknown list-command-examples tag filter %q", strings.Join(unknownTags, ","))
395+
}
396+
267397
return commandExampleManifestReport{
268398
Version: report.Version,
269399
ExampleCount: len(filtered),

0 commit comments

Comments
 (0)