← back to Cli Printing Press
fix(cli): scorer false positives, pagination param plumbing, and catalog proxy_routes (#117)
5094562d123a9f210200c123eefeb68e8aceeab7 · 2026-04-03 16:19:24 -0700 · Trevin Chow
Dogfood's command-tree check used lowercase matching (apigetcategory) which
missed kebab-case subcommands (api get-category) in help output — causing
15 false "unregistered command" reports per run. Now uses camelToKebab
conversion with suffix matching to handle parent-child command hierarchies.
Verify's syntheticArgValue returned "mock-value" for enum-constrained
positional args like <type>, causing 5 false exec_fail results. Added
mappings for common placeholder names (type, resource, format, category,
action, status).
The sync template hardcoded cursorParam: "after" even though the profiler
correctly detects offset vs cursor pagination. Now passes the profiler's
PaginationProfile to the template so generated sync code uses the
detected param.
Added proxy_routes field to catalog schema so proxy-envelope APIs can
declare service-to-path mappings once. The client.go template already
handles .ProxyRoutes — it just needed data from the catalog. Updated
postman-explore entry with /search-all → search, / → publishing.
Verified by regenerating postman-explore-pp-cli: serviceForPath correctly
routes by prefix, determinePaginationDefaults uses "offset", dogfood
reports 0 unregistered commands, verify passes 100%.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
M catalog/postman-explore.yamlA docs/plans/2026-04-03-003-fix-scorer-pagination-proxy-routes-plan.mdA docs/retros/2026-04-03-postman-explore-run2-retro.mdM internal/catalog/catalog.goM internal/cli/root.goM internal/generator/generator.goM internal/generator/templates/sync.go.tmplM internal/pipeline/dogfood.goM internal/pipeline/dogfood_test.goM internal/pipeline/runtime.goM internal/pipeline/runtime_test.go
Diff
commit 5094562d123a9f210200c123eefeb68e8aceeab7
Author: Trevin Chow <trevin@trevinchow.com>
Date: Fri Apr 3 16:19:24 2026 -0700
fix(cli): scorer false positives, pagination param plumbing, and catalog proxy_routes (#117)
Dogfood's command-tree check used lowercase matching (apigetcategory) which
missed kebab-case subcommands (api get-category) in help output — causing
15 false "unregistered command" reports per run. Now uses camelToKebab
conversion with suffix matching to handle parent-child command hierarchies.
Verify's syntheticArgValue returned "mock-value" for enum-constrained
positional args like <type>, causing 5 false exec_fail results. Added
mappings for common placeholder names (type, resource, format, category,
action, status).
The sync template hardcoded cursorParam: "after" even though the profiler
correctly detects offset vs cursor pagination. Now passes the profiler's
PaginationProfile to the template so generated sync code uses the
detected param.
Added proxy_routes field to catalog schema so proxy-envelope APIs can
declare service-to-path mappings once. The client.go template already
handles .ProxyRoutes — it just needed data from the catalog. Updated
postman-explore entry with /search-all → search, / → publishing.
Verified by regenerating postman-explore-pp-cli: serviceForPath correctly
routes by prefix, determinePaginationDefaults uses "offset", dogfood
reports 0 unregistered commands, verify passes 100%.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
catalog/postman-explore.yaml | 3 +
...-003-fix-scorer-pagination-proxy-routes-plan.md | 235 +++++++++++++++++++++
.../2026-04-03-postman-explore-run2-retro.md | 213 +++++++++++++++++++
internal/catalog/catalog.go | 3 +
internal/cli/root.go | 20 ++
internal/generator/generator.go | 2 +
internal/generator/templates/sync.go.tmpl | 10 +-
internal/pipeline/dogfood.go | 33 ++-
internal/pipeline/dogfood_test.go | 25 +++
internal/pipeline/runtime.go | 12 ++
internal/pipeline/runtime_test.go | 26 +++
11 files changed, 570 insertions(+), 12 deletions(-)
diff --git a/catalog/postman-explore.yaml b/catalog/postman-explore.yaml
index 4138aafc..e8ff3ef1 100644
--- a/catalog/postman-explore.yaml
+++ b/catalog/postman-explore.yaml
@@ -9,6 +9,9 @@ tier: community
spec_source: sniffed
auth_required: false
client_pattern: proxy-envelope
+proxy_routes:
+ /search-all: search
+ /: publishing
verified_date: "2026-03-29"
homepage: https://www.postman.com/explore
notes: >
diff --git a/docs/plans/2026-04-03-003-fix-scorer-pagination-proxy-routes-plan.md b/docs/plans/2026-04-03-003-fix-scorer-pagination-proxy-routes-plan.md
new file mode 100644
index 00000000..6d1b4131
--- /dev/null
+++ b/docs/plans/2026-04-03-003-fix-scorer-pagination-proxy-routes-plan.md
@@ -0,0 +1,235 @@
+---
+title: "fix: Scorer false positives, pagination param plumbing, and catalog proxy_routes"
+type: fix
+status: active
+date: 2026-04-03
+origin: docs/retros/2026-04-03-postman-explore-run2-retro.md
+---
+
+# fix: Scorer false positives, pagination param plumbing, and catalog proxy_routes
+
+## Overview
+
+Three small, independent fixes surfaced by the Postman Explore Run 2 retro. Each addresses a different layer of the printing press:
+
+1. **Dogfood and verify false positives** — dogfood's command-tree check fails on subcommands (CamelCase→lowercase doesn't match kebab-case help output), and verify's synthetic arg generator produces `"mock-value"` for enum-constrained positional args.
+2. **Sync template pagination** — the generated `determinePaginationDefaults()` hardcodes `cursorParam: "after"` even though the profiler correctly detects `"offset"` for offset-paginated APIs.
+3. **Catalog proxy_routes** — proxy-envelope APIs with multiple backend services (like Postman Explore's "publishing" + "search") have no way to declare service routing in the catalog. The generator template already handles `.ProxyRoutes` — it just needs data.
+
+## Problem Frame
+
+Every printed CLI goes through dogfood, verify, and scorecard. False positives in these tools waste time on non-issues and mask real problems. The Postman Explore run produced 15 false "unregistered commands" (dogfood) and 5 false exec_fail results (verify) — noise that obscured legitimate gaps.
+
+Separately, the sync template's hardcoded cursor pagination breaks offset-paginated APIs (sync never advances past page 1), and proxy-envelope APIs require manual `serviceForPath` edits every time.
+
+(see origin: `docs/retros/2026-04-03-postman-explore-run2-retro.md`, findings #3, #4, #5, #1)
+
+## Requirements Trace
+
+- R1. Dogfood's command-tree check must not produce false "unregistered" results for subcommands registered via cobra's `AddCommand`
+- R2. Verify must provide valid synthetic arg values for common enum-constrained positional arg placeholders (type, resource, format, kind)
+- R3. Generated `determinePaginationDefaults()` must use the profiler-detected cursor/offset param, not a hardcoded default
+- R4. Catalog entries for proxy-envelope APIs can declare `proxy_routes` to map path prefixes to service names, and the generator must consume them
+
+## Scope Boundaries
+
+- Do not change verify's execution logic or command discovery — only fix arg synthesis and name matching
+- Do not change the profiler's pagination detection — it already works correctly
+- Do not auto-detect proxy routes from specs — require explicit catalog declaration
+- Do not address finding #2 (enum-parameterized sync resource expansion) — that needs design work and is deferred to "Do Next"
+- Do not address finding #7 (user-friendly wrapper commands) — that's a large template gap
+
+## Context & Research
+
+### Relevant Code and Patterns
+
+- `internal/pipeline/dogfood.go:960-1030` — `checkCommandTree()` scans for `func new*Cmd()`, lowercases CamelCase suffix, matches against `--help` output. Uses `strings.Contains(helpLower, cmdName)`.
+- `internal/pipeline/runtime.go:440-458` — `syntheticArgValue()` maps placeholder names to test values. Falls back to `"mock-value"`.
+- `internal/generator/templates/sync.go.tmpl:296-304` — `determinePaginationDefaults()` hardcodes `cursorParam: "after"`.
+- `internal/generator/generator.go:387-398` — template data struct passes `SyncableResources` and `SearchableFields` but **not** `Pagination`.
+- `internal/profiler/profiler.go:39-45` — `PaginationProfile` struct with `CursorParam`, `PageSizeParam`, `SinceParam`, `ItemsKey`, `DefaultPageSize`.
+- `internal/catalog/catalog.go:65-87` — `Entry` struct has `ClientPattern` but no `ProxyRoutes`.
+- `internal/spec/spec.go:19-20` — `APISpec` already has `ProxyRoutes map[string]string` field.
+- `internal/generator/templates/client.go.tmpl:121-150` — template already handles `.ProxyRoutes` with longest-prefix matching. Just needs data.
+
+### Institutional Learnings
+
+- Previous retros (steam, redfin) also surfaced pagination issues but focused on cursor-based APIs. This is the first offset-pagination finding.
+- The proxy-envelope pattern was added for Postman Explore in the initial run. The template already supports `.ProxyRoutes` — it was just never populated from catalog data.
+
+## Key Technical Decisions
+
+- **CamelCase→kebab conversion for dogfood**: Insert hyphens before uppercase letters in the CamelCase suffix, then lowercase the result. `ApiGetCategory` → `api-get-category`. This matches cobra's convention and the generated command names. Alternatives considered: splitting into individual words and searching for each — rejected because it would produce false positives (e.g., `Category` appears in unrelated help text).
+
+- **Synthetic arg inference from help text vs static mapping**: For now, expand `syntheticArgValue()` with common placeholder→value mappings. The retro suggests parsing help text for valid values as a better long-term approach, but that's more complex and the static mappings cover the common cases today.
+
+- **Pagination template plumbing**: Pass the entire `PaginationProfile` struct to the sync template data. The template already has the `determinePaginationDefaults()` function — change it to use template values instead of hardcoded strings.
+
+- **ProxyRoutes in catalog**: Add `proxy_routes` as `map[string]string` to the catalog `Entry` struct. When loading a catalog entry, copy its `ProxyRoutes` into the `APISpec.ProxyRoutes` field (which already exists). No validation needed beyond the existing client_pattern check.
+
+## Open Questions
+
+### Resolved During Planning
+
+- **Should dogfood also check the cobra `Use:` field directly?** No — parsing Go source for string literals is fragile. CamelCase→kebab conversion from function names is more reliable and matches the generator's naming convention.
+- **Should `syntheticArgValue` try all enum values until one succeeds?** No — that would be slow and requires building the binary. Static mappings for common placeholders are sufficient and fast.
+
+### Deferred to Implementation
+
+- **Exact CamelCase→kebab regex**: The implementation should handle edge cases like consecutive uppercase letters (`APIKey` → `api-key` not `a-p-i-key`). Use the same convention as cobra/Go naming.
+
+## Implementation Units
+
+- [ ] **Unit 1: Fix dogfood CamelCase→kebab command matching**
+
+**Goal:** Eliminate false "unregistered commands" for subcommands
+
+**Requirements:** R1
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `internal/pipeline/dogfood.go`
+- Test: `internal/pipeline/dogfood_test.go`
+
+**Approach:**
+- In `checkCommandTree()`, after extracting the CamelCase suffix from `newXxxCmd()`, convert it to kebab-case before matching. Insert a hyphen before each uppercase letter (handling consecutive caps like API → api), then lowercase.
+- The matching at line 1023 (`strings.Contains(helpLower, cmdName)`) then works because `api-get-category` appears in the help output under the `api` subcommand.
+
+**Patterns to follow:**
+- Existing `checkCommandTree` logic and test structure in `dogfood_test.go`
+
+**Test scenarios:**
+- Happy path: CLI with `newApiGetCategoryCmd()` → dogfood detects it as registered (matches `api` + `get-category` in help)
+- Happy path: CLI with `newAuthLoginCmd()` → detected as registered
+- Edge case: `newVersionCliCmd()` → correctly matches `version` in help
+- Edge case: CLI with single-word command `newSyncCmd()` → still works (no hyphens needed)
+- Integration: Run dogfood on postman-explore-pp-cli → 0 unregistered commands
+
+**Verification:**
+- `go test ./internal/pipeline/ -run TestCheckCommandTree` passes
+- Dogfood on a CLI with subcommands reports 0 unregistered
+
+---
+
+- [ ] **Unit 2: Expand syntheticArgValue for enum-constrained args**
+
+**Goal:** Verify passes exec test for commands with enum-constrained positional args
+
+**Requirements:** R2
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `internal/pipeline/runtime.go`
+- Test: `internal/pipeline/runtime_test.go`
+
+**Approach:**
+- Add mappings to `syntheticArgValue()` for common enum-constrained placeholder names: `"type"` → `"collection"`, `"resource"` → `"items"`, `"format"` → `"json"`, `"kind"` → `"default"`, `"entity"` → `"item"`, `"category"` → `"general"`, `"action"` → `"list"`.
+- These are common generic placeholder names used across many CLIs.
+
+**Patterns to follow:**
+- Existing switch cases in `syntheticArgValue()`
+
+**Test scenarios:**
+- Happy path: `syntheticArgValue("type")` returns `"collection"`
+- Happy path: `syntheticArgValue("resource")` returns `"items"`
+- Edge case: `syntheticArgValue("unknown-thing")` still returns `"mock-value"` (default fallback unchanged)
+- Edge case: existing mappings unchanged — `syntheticArgValue("query")` still returns `"mock-query"`
+
+**Verification:**
+- `go test ./internal/pipeline/ -run TestSyntheticArgValue` passes
+
+---
+
+- [ ] **Unit 3: Wire profiler pagination params into sync template**
+
+**Goal:** Generated sync uses detected pagination param instead of hardcoded "after"
+
+**Requirements:** R3
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `internal/generator/templates/sync.go.tmpl`
+- Modify: `internal/generator/generator.go`
+- Test: `internal/generator/generator_test.go`
+
+**Approach:**
+- In `generator.go`, add `Pagination profiler.PaginationProfile` to the sync template data struct (alongside `SyncableResources` and `SearchableFields`). Populate from `g.profile.Pagination`.
+- In `sync.go.tmpl`, replace the hardcoded `determinePaginationDefaults()` body with template values: `cursorParam: "{{.Pagination.CursorParam}}"`, `limitParam: "{{.Pagination.PageSizeParam}}"`, `limit: {{.Pagination.DefaultPageSize}}`.
+- Also template `determineSinceParam()` to use `{{.Pagination.SinceParam}}` with fallback to `"since"`.
+
+**Patterns to follow:**
+- How `SyncableResources` is already passed through the template data struct in `generator.go:387-398`
+
+**Test scenarios:**
+- Happy path: Generate from spec with `offset` pagination param → `determinePaginationDefaults()` returns `cursorParam: "offset"`
+- Happy path: Generate from spec with `after` pagination param → returns `cursorParam: "after"`
+- Edge case: Spec with no detected pagination → profiler defaults to `"after"` and `100` → template emits those defaults
+- Integration: Generated CLI compiles and passes quality gates after template change
+
+**Verification:**
+- `go test ./internal/generator/ -run TestGenerate` passes
+- Generated `sync.go` contains the profiler-detected param, not hardcoded `"after"`
+
+---
+
+- [ ] **Unit 4: Add proxy_routes to catalog schema and wire to generator**
+
+**Goal:** Proxy-envelope APIs can declare service routing in their catalog entry
+
+**Requirements:** R4
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `internal/catalog/catalog.go`
+- Modify: `catalog/postman-explore.yaml`
+- Modify: `internal/generator/generator.go` (or wherever catalog → APISpec mapping happens)
+- Test: `internal/catalog/catalog_test.go`
+- Test: `internal/generator/generator_test.go`
+
+**Approach:**
+- Add `ProxyRoutes map[string]string` field to the catalog `Entry` struct with yaml tag `proxy_routes,omitempty`.
+- Update `postman-explore.yaml` to include `proxy_routes: {"/search-all": "search", "/": "publishing"}`.
+- In the code path where catalog entries are loaded into `APISpec`, copy `entry.ProxyRoutes` to `spec.ProxyRoutes`. The `APISpec` struct already has `ProxyRoutes map[string]string` (spec.go:20).
+- The `client.go.tmpl` template already handles `.ProxyRoutes` with longest-prefix matching — no template changes needed.
+- Rebuild the binary after adding the catalog entry (catalog entries are embedded via `catalog.FS`).
+
+**Patterns to follow:**
+- How `ClientPattern` flows from catalog entry to APISpec (existing pattern)
+- How `AuthRequired` was added to the catalog Entry struct
+
+**Test scenarios:**
+- Happy path: Parse catalog entry with `proxy_routes` → Entry.ProxyRoutes populated correctly
+- Happy path: Generate from postman-explore catalog → `serviceForPath("/search-all")` returns `"search"`, `serviceForPath("/v1/api/team")` returns `"publishing"`
+- Edge case: Catalog entry without `proxy_routes` → Entry.ProxyRoutes is nil, serviceForPath returns API name slug (unchanged behavior)
+- Edge case: Catalog validation passes with and without `proxy_routes`
+
+**Verification:**
+- `go test ./internal/catalog/` passes
+- `go test ./internal/generator/` passes
+- `printing-press catalog show postman-explore --json` shows proxy_routes
+
+## System-Wide Impact
+
+- **Interaction graph:** Dogfood and verify are called by `printing-press dogfood`, `printing-press verify`, and the skill's shipcheck block. Fixing false positives improves all three callers.
+- **Error propagation:** No new error paths. All changes are in existing code paths.
+- **State lifecycle risks:** None — these are stateless functions.
+- **API surface parity:** The catalog's `proxy_routes` field is new but backward-compatible (omitempty). Existing catalog entries are unchanged.
+- **Unchanged invariants:** The profiler's pagination detection logic is not modified. The generator's template rendering pipeline is not modified beyond adding one field to the data struct. The client.go.tmpl proxy-envelope template is not modified.
+
+## Risks & Dependencies
+
+| Risk | Mitigation |
+|------|------------|
+| CamelCase→kebab conversion produces wrong results for edge cases (e.g., `APIv2List` → `ap-iv2-list` instead of `api-v2-list`) | Use Go's established camelCase-to-kebab pattern (split on uppercase boundaries, handle consecutive caps). Test with real function names from generated CLIs. |
+| Adding `Pagination` to sync template data breaks existing generated CLIs | The template uses `{{.Pagination.CursorParam}}` which evaluates to `""` if the field is zero-value. The `determinePaginationDefaults` function already has defaults, so even if Pagination is empty, the function still returns sensible values. |
+| Catalog entry rebuild required after adding proxy_routes to postman-explore.yaml | Document that `go build` must be run after catalog changes. This is already documented in CLAUDE.md. |
+
+## Sources & References
+
+- **Origin document:** [docs/retros/2026-04-03-postman-explore-run2-retro.md](docs/retros/2026-04-03-postman-explore-run2-retro.md)
+- Related code: `internal/pipeline/dogfood.go`, `internal/pipeline/runtime.go`, `internal/generator/templates/sync.go.tmpl`, `internal/catalog/catalog.go`
+- Related retros: `docs/retros/2026-03-30-postman-explore-retro.md` (first run)
diff --git a/docs/retros/2026-04-03-postman-explore-run2-retro.md b/docs/retros/2026-04-03-postman-explore-run2-retro.md
new file mode 100644
index 00000000..f937db46
--- /dev/null
+++ b/docs/retros/2026-04-03-postman-explore-run2-retro.md
@@ -0,0 +1,213 @@
+# Printing Press Retro: Postman Explore (Run 2)
+
+## Session Stats
+- API: postman-explore
+- Spec source: catalog (community/sniffed, from previous run)
+- Scorecard: 90/100 Grade A
+- Verify pass rate: 71%
+- Fix loops: 1 (no improvement — failures are positional-arg false negatives)
+- Manual code edits: 6
+- Features built from scratch: 8
+
+## Findings
+
+### 1. serviceForPath() returns API slug instead of actual service name (Bug)
+
+- **What happened:** Generator emitted `return "postman-explore"` as the default service name. The proxy-envelope API uses two services: "publishing" for browse endpoints and "search" for search endpoints. Had to manually rewrite to route by path prefix.
+- **Root cause:** Generator template (`client.go.tmpl`) uses `.ProxyRoutes` for path-prefix-to-service mapping, but `.ProxyRoutes` was empty. The profiler/parser didn't detect separate services because the spec has a single server URL and no `x-proxy-routes` annotation.
+- **Cross-API check:** Any proxy-envelope API with multiple backend services. The Postman Explore API has this pattern (publishing + search). Other APIs with gateway/proxy patterns would too.
+- **Frequency:** subclass: proxy-envelope APIs with multiple services
+- **Fallback if machine doesn't fix it:** Claude must manually detect from spec structure or live probing which paths map to which services. Reliability: sometimes — Claude might miss the split, especially if the spec doesn't annotate services.
+- **Worth a machine fix?** Yes. The catalog entry already has `ClientPattern: "proxy-envelope"` and `Notes` describing the proxy. The catalog could include a `ProxyRoutes` field mapping path prefixes to service names.
+- **Inherent or fixable:** Fixable. The catalog YAML could carry `proxy_routes: {"/search-all": "search", "/": "publishing"}` and the generator could consume it.
+- **Durable fix:** Add `proxy_routes` field to catalog schema. When present, populate `.ProxyRoutes` in the generator context. The template already handles `.ProxyRoutes` — it just needs data.
+ - Condition: spec has `proxy_routes` in catalog entry
+ - Guard: Standard REST APIs without proxy pattern — no change
+ - Frequency estimate: ~5% of APIs use proxy-envelope
+- **Test:** Generate from postman-explore catalog entry → verify `serviceForPath` routes "/search-all" to "search" and "/" to "publishing". Negative: generate from Stripe → verify single service name.
+- **Evidence:** Client returned `invalidServiceError` for service "postman-explore"; had to probe "publishing" and "search" manually.
+
+### 2. defaultSyncResources() derived wrong resource list (Bug)
+
+- **What happened:** Generator produced `defaultSyncResources()` returning `["api"]` — a single entry mapped to `/v1/api/team`. The API has 6 entity types to sync (collection, workspace, api, flow, team, category). Had to manually rewrite to list all 6 with correct API paths.
+- **Root cause:** Profiler's resource name extraction from paths. Both `/v1/api/networkentity` and `/v1/api/team` share the `/v1/api/` prefix. The profiler likely extracted "api" as the resource name for both, causing a collision where only one survived. The `/v1/api/networkentity` endpoint is parameterized by `entityType` query param — the profiler doesn't recognize that one endpoint represents 4 resources.
+- **Cross-API check:** Any API where one endpoint serves multiple entity types via a query parameter (like `entityType=collection|workspace`). Also APIs where multiple endpoints share a parent path segment.
+- **Frequency:** subclass: APIs with multi-entity list endpoints (enum-parameterized)
+- **Fallback if machine doesn't fix it:** Claude must read the spec, identify the entityType enum, and manually expand to separate sync resources. Reliability: sometimes — Claude might not realize the enum represents separate resources.
+- **Worth a machine fix?** Yes, but complex. The profiler would need to detect enum params on list endpoints and expand each enum value into a separate sync resource.
+- **Inherent or fixable:** Fixable with design work. The profiler could detect enum query params on list endpoints and expand: `/v1/api/networkentity?entityType=collection` → resource "collection", etc.
+- **Durable fix:** In the profiler's syncable resource detection: when a GET list endpoint has a required enum query parameter, expand each enum value into a separate sync resource. Include the enum value in the resource path.
+ - Condition: List endpoint has required enum param
+ - Guard: Endpoints without enum params — unchanged
+ - Frequency estimate: ~10% of APIs have enum-parameterized list endpoints
+- **Test:** Parse postman-explore spec → profiler produces 5 syncable resources (collection, workspace, api, flow, team) not 1. Negative: parse Stripe → unchanged (no enum-parameterized list endpoints).
+- **Evidence:** `defaultSyncResources()` returned `["api"]`, mapped to `/v1/api/team`.
+
+### 3. determinePaginationDefaults() hardcoded to cursor pagination (Assumption mismatch)
+
+- **What happened:** Generator hardcoded `{cursorParam: "after", limitParam: "limit", limit: 100}`. The Postman Explore API uses offset-based pagination (`offset` param). Had to change `cursorParam` from "after" to "offset".
+- **Scorer correct?** N/A — not surfaced by a score penalty.
+- **Root cause:** The template (`sync.go.tmpl`) hardcodes defaults instead of using the profiler's detected pagination params. The profiler DOES detect the correct params via `mostCommon()` analysis, but the template doesn't consume `p.Pagination.CursorParam`.
+- **Cross-API check:** Offset pagination is extremely common — ~40% of REST APIs use offset/limit instead of cursor/after.
+- **Frequency:** most APIs (40%+ use offset pagination)
+- **Fallback if machine doesn't fix it:** Claude manually edits one line. Reliability: usually — but it's easy to miss, and the wrong pagination param means sync never advances past page 1.
+- **Worth a machine fix?** Absolutely. The profiler already detects the correct param. The template just needs to use it.
+- **Inherent or fixable:** Fixable. Trivial template change.
+- **Durable fix:** In `sync.go.tmpl`, replace hardcoded `cursorParam: "after"` with `cursorParam: "{{.Pagination.CursorParam}}"`. The profiler already populates `Pagination.CursorParam` with the detected value.
+- **Test:** Generate from postman-explore spec (has `offset` param) → verify `determinePaginationDefaults()` returns `offset`. Generate from GitHub spec (has `after` param) → verify returns `after`.
+- **Evidence:** Sync used `after=` which the API ignored; entities never paginated.
+
+### 4. Dogfood reports false "unregistered commands" for subcommands (Scorer bug)
+
+- **What happened:** Dogfood reported 15 "unregistered commands" including `apigetcategory`, `apilistcategories`, etc. All of these ARE registered as subcommands (e.g., `api get-category`).
+- **Scorer correct?** No — scorer bug. The detection logic at `dogfood.go:966-1029` scans for `func new*Cmd()` patterns, extracts the CamelCase suffix (e.g., `ApiGetCategory`), lowercases it to `apigetcategory`, then searches for that string in the `--help` output. The help output shows `get-category` (kebab-case), which does NOT contain `apigetcategory`. The matching fails because it doesn't convert CamelCase to kebab-case.
+- **Root cause:** `dogfood.go:982` — `cmdName := strings.ToLower(match[1])` converts `ApiGetCategory` to `apigetcategory` and then checks `strings.Contains(helpLower, cmdName)` at line 1023. The help output contains `get-category` under `api` subcommand, not `apigetcategory`.
+- **Cross-API check:** Every CLI with subcommands (api get-X, auth login, etc.) — this is most CLIs.
+- **Frequency:** every API
+- **Fallback if machine doesn't fix it:** N/A — this is a scorer fix, not a generator fix.
+- **Worth a machine fix?** Yes — fix the scorer.
+- **Durable fix:** In `dogfood.go`, convert the CamelCase function name to kebab-case before matching against help output. E.g., `ApiGetCategory` → `api-get-category` or split on uppercase boundaries and search for each component. Alternatively, search for each word separately: `api`, `get`, `category` all appear in the help output.
+- **Test:** Run dogfood on a CLI with subcommands (api get-X) → no false unregistered commands.
+- **Evidence:** Dogfood reported `apigetcategory, apigetnetworkentitycounts, apilistcategories, apilistnetworkentities, apilistteams` etc. as unregistered.
+
+### 5. Verify fails on commands requiring positional args with enum values (Scorer bug)
+
+- **What happened:** Verify failed 5 commands (browse, open, similar, stale, trending) with `exec_fail`. These commands require positional args. Verify's `inferPositionalArgs()` correctly detected the `<type>` placeholder and mapped it to `"mock-value"` via `syntheticArgValue()`. But `"mock-value"` is rejected by browse's type validation (expects collections/workspaces/apis/flows).
+- **Scorer correct?** Partially. The arg inference works — it correctly finds `<type>` and provides a value. But the default fallback `"mock-value"` is useless for enum-constrained args. For `stale` and `open`, the issue is different — they need a local SQLite DB that doesn't exist during verify.
+- **Root cause:** `runtime.go:441-458` — `syntheticArgValue()` has specific mappings for `query`, `name`, `id`, `region` etc. but no mapping for `type`. The `default` case returns `"mock-value"`.
+- **Cross-API check:** Any CLI with enum-constrained positional args. Common patterns: `browse <type>`, `list <resource>`, `show <format>`.
+- **Frequency:** most APIs — generated CLIs frequently have commands like `browse <type>` or `list <resource>`
+- **Fallback if machine doesn't fix it:** N/A — scorer fix.
+- **Worth a machine fix?** Yes — fix the scorer.
+- **Durable fix:** Two approaches:
+ 1. Add common placeholder names to `syntheticArgValue()`: `"type" → "collection"`, `"resource" → "items"`, `"format" → "json"`
+ 2. Better: parse the command's help output for valid values (many help texts list valid args in the description) and pick the first one
+- **Test:** Run verify on a CLI with `browse <type>` → verify uses a valid type value. No false exec_fail.
+- **Evidence:** Verify ran `browse mock-value` which failed with "unknown type".
+
+### 6. Generator emits spec enum values without live validation (Discovered optimization)
+
+- **What happened:** The spec listed sort values as `[popular, recent, featured, new, week, alltime]`. The live API only accepts `popular` and `featured` — the other 4 return `invalidParamsError`. Had to test each value and fix the command flags.
+- **Root cause:** The spec was reverse-engineered from a previous sniff run and included values observed in the website's JavaScript code, but the API doesn't actually accept all of them server-side.
+- **Cross-API check:** Any sniffed spec — client-side code may reference enum values the server doesn't support.
+- **Frequency:** subclass: sniffed specs
+- **Fallback if machine doesn't fix it:** Claude tests each enum value during verify/live-smoke and fixes invalid ones. Reliability: usually catches it if live testing runs.
+- **Inherent or fixable:** Partially fixable. The verify tool could probe enum values against the live API (when available) and flag invalid ones. But this requires API access during verification.
+- **Durable fix:** In verify's `--fix` mode: for commands with enum flags, try each enum value and mark invalid ones. Remove invalid values from the flag description.
+- **Test:** Verify against postman-explore with live API → detect and report invalid sort values.
+- **Evidence:** `browse collections --sort week` returned HTTP 400 `invalidParamsError`.
+
+### 7. No user-friendly wrapper commands generated for nested API commands (Template gap)
+
+- **What happened:** Generator produced `api list-network-entities` and `search-all search_all` — functional but terrible UX. Had to write 8 user-friendly wrapper commands from scratch (search, browse, categories, stats, open, trending, stale, similar).
+- **Root cause:** Generator emits commands directly from operationIds. For APIs with deeply nested or technical command names, this produces unusable CLIs. There's no mechanism to generate "user-friendly alias" commands that wrap the raw API commands.
+- **Cross-API check:** Every API — the operationId-derived names are always developer-oriented, not user-oriented.
+- **Frequency:** every API
+- **Fallback if machine doesn't fix it:** Claude builds wrapper commands during Phase 3 every time. Reliability: always catches it (it's in the absorb manifest), but it's significant work.
+- **Worth a machine fix?** Yes, but complex. The generator would need to identify "primary user workflows" from the spec and emit top-level convenience commands.
+- **Inherent or fixable:** Partially fixable. A post-generation step could identify the most common operations (list, search, get) and emit user-friendly top-level commands. But naming them well requires product judgment that's hard to automate.
+- **Durable fix:** Two levels:
+ 1. **Near-term:** Emit promoted/alias commands from Phase 1.5 absorb manifest features. The skill already creates this manifest — the generator could consume it.
+ 2. **Long-term:** Auto-detect primary entity types and emit standard commands (browse, search, inspect, open) as top-level aliases.
+- **Test:** Generate from any spec → verify top-level commands include user-friendly names, not just operationId-derived ones.
+- **Evidence:** Built 8 commands by hand in Phase 3.
+
+### 8. Store migration doesn't create entity-specific tables for sniffed specs (Missing scaffolding)
+
+- **What happened:** Generator created generic `resources` + `resources_fts` tables. Had to manually add 5 tables: entities (with extracted metric columns), categories, teams, metric_snapshots, watchlist.
+- **Root cause:** Schema builder (`schema_builder.go`) scores data gravity from spec schemas. The Postman Explore spec has relatively simple schemas (no foreign keys, few temporal fields), scoring below the threshold for entity-specific table generation. Also, the schema builder doesn't extract metric arrays into individual columns.
+- **Cross-API check:** APIs with nested metric arrays or complex response structures that benefit from column extraction.
+- **Frequency:** subclass: APIs with metric/analytics data in array format
+- **Worth a machine fix?** Partially. The generic `resources` table works for most APIs. Entity-specific tables with extracted columns are a transcendence feature that requires domain knowledge (knowing which metrics matter). Hard to generalize.
+- **Inherent or fixable:** Partially fixable. The schema builder could detect arrays of `{name, value}` objects (metric patterns) and extract them into columns. But the watchlist and metric_snapshots tables are novel features that the generator shouldn't try to anticipate.
+- **Durable fix:** In `schema_builder.go`: when a schema contains an array of objects with `metricName`/`metricValue` pattern, create extracted columns for each enum value.
+ - Condition: Schema has metric-array pattern
+ - Guard: APIs without metrics — unchanged
+- **Test:** Parse postman-explore spec → store migration includes metric columns. Parse Stripe → unchanged.
+- **Evidence:** Manually added entities table with fork_count, view_count, etc.
+
+## Prioritized Improvements
+
+### Fix the Scorer (highest priority)
+
+| # | Scorer | Bug | Impact | Fix target |
+|---|--------|-----|--------|------------|
+| 4 | Dogfood | CamelCase→lowercase matching misses kebab-case subcommands | 15 false unregistered commands every run | `internal/pipeline/dogfood.go:982` — add CamelCase→kebab conversion |
+| 5 | Verify | `syntheticArgValue()` default `"mock-value"` fails enum-constrained args | 5 false exec_fail per run | `internal/pipeline/runtime.go:441` — add "type"→"collection" mapping |
+
+### Do Now
+
+| # | Fix | Component | Frequency | Fallback Reliability | Complexity | Guards |
+|---|-----|-----------|-----------|---------------------|------------|--------|
+| 3 | Use profiler's detected pagination params in sync template | `sync.go.tmpl` | most APIs | usually | small | None needed — profiler already detects |
+| 1 | Add proxy_routes to catalog schema | `catalog/`, `generator/` | proxy-envelope | sometimes | small | Only when catalog has proxy_routes |
+
+### Do Next (needs design)
+
+| # | Fix | Component | Frequency | Fallback Reliability | Complexity | Guards |
+|---|-----|-----------|-----------|---------------------|------------|--------|
+| 2 | Expand enum-parameterized list endpoints to multiple sync resources | profiler | 10% APIs | sometimes | medium | Only for required enum params on GET list |
+| 7 | Emit user-friendly wrapper commands from absorb manifest | generator | every API | always (Claude builds) | large | Needs product judgment heuristics |
+
+### Skip
+
+| # | Fix | Why unlikely to recur |
+|---|-----|----------------------|
+| 6 | Validate spec enum values against live API | Requires API access during verify; most specs have correct enums. Sniffed specs are the exception, and verify --fix already catches runtime failures. |
+| 8 | Extract metric arrays into store columns | Metric-array pattern is uncommon across APIs. The generic resources table is sufficient for most cases. |
+
+## Work Units
+
+### WU-1: Fix scorer false positives (findings #4, #5)
+
+- **Goal:** Eliminate false dogfood "unregistered commands" and false verify exec_fail for enum-constrained args
+- **Target files:**
+ - `internal/pipeline/dogfood.go` (line 982 — CamelCase→kebab matching)
+ - `internal/pipeline/runtime.go` (line 441 — syntheticArgValue additions)
+- **Acceptance criteria:**
+ - Run dogfood on postman-explore-pp-cli → 0 unregistered commands
+ - Run verify on postman-explore-pp-cli → browse/trending pass exec test
+ - Run dogfood on steam-web-pp-cli → no regression (subcommands still detected)
+- **Scope boundary:** Don't change verify's exec logic — only fix name matching and arg synthesis
+- **Complexity:** small (2 files, straightforward string conversion)
+
+### WU-2: Wire profiler pagination params into sync template (finding #3)
+
+- **Goal:** Generated sync uses the profiler-detected pagination param (offset vs cursor) instead of hardcoded "after"
+- **Target files:**
+ - `internal/generator/templates/sync.go.tmpl` (determinePaginationDefaults function)
+ - `internal/generator/generator.go` (template data struct — verify Pagination is passed)
+- **Acceptance criteria:**
+ - Generate from postman-explore spec (offset pagination) → `determinePaginationDefaults()` returns `{cursorParam: "offset"}`
+ - Generate from GitHub spec (cursor pagination) → returns `{cursorParam: "after"}`
+- **Scope boundary:** Don't change the profiler's detection logic — it already works correctly
+- **Complexity:** small (1 template change, verify data plumbing)
+
+### WU-3: Add proxy_routes to catalog schema (finding #1)
+
+- **Goal:** Proxy-envelope APIs can declare service-to-path mappings in their catalog entry, and the generator uses them
+- **Target files:**
+ - `internal/catalog/catalog.go` (add ProxyRoutes field)
+ - `catalog/postman-explore.yaml` (add proxy_routes)
+ - `internal/generator/generator.go` (populate .ProxyRoutes from catalog)
+ - `internal/generator/templates/client.go.tmpl` (verify .ProxyRoutes is consumed)
+- **Acceptance criteria:**
+ - postman-explore catalog entry has `proxy_routes: {"/search-all": "search", "/": "publishing"}`
+ - Generated client routes /search-all to "search" and / to "publishing"
+ - APIs without proxy_routes — unchanged
+- **Scope boundary:** Don't auto-detect proxy routes from specs — require explicit catalog declaration
+- **Complexity:** small (4 files, straightforward field addition)
+
+## Anti-patterns
+
+- **Trusting spec enum values blindly** — Sniffed specs may include client-side enum values the server rejects. Always test enum values against the live API when possible.
+- **Assuming cursor pagination as default** — Offset pagination is equally common. Default to the detected value, not a hardcoded one.
+
+## What the Machine Got Right
+
+- **Proxy-envelope client pattern** — The `--client-pattern proxy-envelope` flag correctly generated the proxy envelope wrapping (POST with service/method/path body). Only the service routing was wrong.
+- **Quality gates** — All 7 gates passed on first generation. The generator produces compilable, runnable code reliably.
+- **Data layer scaffolding** — Generic resources table + FTS5 + sync infrastructure work out of the box. Entity-specific tables were added for transcendence but the foundation was solid.
+- **Agent-native defaults** — --json, --compact, --agent, --dry-run, --select all work correctly with zero manual setup. Agent Native scored 10/10.
+- **Adaptive rate limiter** — The proactive rate limiter with 429 backoff worked perfectly against the Postman proxy API.
+- **Response cache** — 5-minute GET cache prevented duplicate API calls during development.
diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go
index b5f69edf..ba44da58 100644
--- a/internal/catalog/catalog.go
+++ b/internal/catalog/catalog.go
@@ -84,6 +84,9 @@ type Entry struct {
// ClientPattern describes the HTTP client pattern needed. Empty defaults to "rest".
// Values: rest, proxy-envelope, graphql.
ClientPattern string `yaml:"client_pattern,omitempty"`
+ // ProxyRoutes maps path prefixes to backend service names for proxy-envelope APIs.
+ // Only relevant when ClientPattern is "proxy-envelope".
+ ProxyRoutes map[string]string `yaml:"proxy_routes,omitempty"`
}
func ParseEntry(data []byte) (*Entry, error) {
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 96c98ff6..6f0154d7 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -13,6 +13,8 @@ import (
"strings"
"time"
+ catalogfs "github.com/mvanhorn/cli-printing-press/catalog"
+ "github.com/mvanhorn/cli-printing-press/internal/catalog"
"github.com/mvanhorn/cli-printing-press/internal/docspec"
"github.com/mvanhorn/cli-printing-press/internal/generator"
"github.com/mvanhorn/cli-printing-press/internal/graphql"
@@ -154,6 +156,7 @@ func newGenerateCmd() *cobra.Command {
return &ExitError{Code: ExitInputError, Err: err}
}
+ enrichSpecFromCatalog(parsed)
gen := generator.New(parsed, absOut)
loadResearchSources(gen, researchDir)
if err := gen.Generate(); err != nil {
@@ -280,6 +283,7 @@ func newGenerateCmd() *cobra.Command {
return &ExitError{Code: ExitInputError, Err: err}
}
+ enrichSpecFromCatalog(apiSpec)
gen := generator.New(apiSpec, absOut)
loadResearchSources(gen, researchDir)
if err := gen.Generate(); err != nil {
@@ -666,3 +670,19 @@ func loadResearchSources(gen *generator.Generator, researchDir string) {
discoveryDir := filepath.Join(researchDir, "discovery")
gen.DiscoveryPages = pipeline.ParseDiscoveryPages(discoveryDir)
}
+
+// enrichSpecFromCatalog looks up the API in the embedded catalog and copies
+// ProxyRoutes into the spec if present. This allows catalog entries to declare
+// service routing for proxy-envelope APIs without requiring CLI flags.
+func enrichSpecFromCatalog(apiSpec *spec.APISpec) {
+ if apiSpec == nil || apiSpec.Name == "" {
+ return
+ }
+ entry, err := catalog.LookupFS(catalogfs.FS, apiSpec.Name)
+ if err != nil {
+ return
+ }
+ if len(entry.ProxyRoutes) > 0 && len(apiSpec.ProxyRoutes) == 0 {
+ apiSpec.ProxyRoutes = entry.ProxyRoutes
+ }
+}
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index ba3745aa..5bfb3f8f 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -415,11 +415,13 @@ func (g *Generator) Generate() error {
SyncableResources []profiler.SyncableResource
SearchableFields map[string][]string
Tables []TableDef
+ Pagination profiler.PaginationProfile
}{
APISpec: g.Spec,
SyncableResources: g.profile.SyncableResources,
SearchableFields: g.profile.SearchableFields,
Tables: schema,
+ Pagination: g.profile.Pagination,
}
for _, tmplName := range g.VisionSet.TemplateNames() {
diff --git a/internal/generator/templates/sync.go.tmpl b/internal/generator/templates/sync.go.tmpl
index 1f4b5e42..bccbd434 100644
--- a/internal/generator/templates/sync.go.tmpl
+++ b/internal/generator/templates/sync.go.tmpl
@@ -294,18 +294,18 @@ type paginationDefaults struct {
}
// determinePaginationDefaults returns the pagination parameter names to use.
-// Uses sensible defaults matching common API conventions.
+// Values are detected from the API spec by the profiler at generation time.
func determinePaginationDefaults() paginationDefaults {
return paginationDefaults{
- cursorParam: "after",
- limitParam: "limit",
- limit: 100,
+ cursorParam: "{{if .Pagination.CursorParam}}{{.Pagination.CursorParam}}{{else}}after{{end}}",
+ limitParam: "{{if .Pagination.PageSizeParam}}{{.Pagination.PageSizeParam}}{{else}}limit{{end}}",
+ limit: {{if .Pagination.DefaultPageSize}}{{.Pagination.DefaultPageSize}}{{else}}100{{end}},
}
}
// determineSinceParam returns the query parameter name for incremental sync filtering.
func determineSinceParam() string {
- return "since"
+ return "{{if .Pagination.SinceParam}}{{.Pagination.SinceParam}}{{else}}since{{end}}"
}
// extractPageItems attempts to extract an array of items and pagination cursor from a response.
diff --git a/internal/pipeline/dogfood.go b/internal/pipeline/dogfood.go
index ec729bf7..d6cc2586 100644
--- a/internal/pipeline/dogfood.go
+++ b/internal/pipeline/dogfood.go
@@ -973,12 +973,10 @@ func checkCommandTree(dir string) CommandTreeResult {
}
matches := cmdFuncRe.FindAllStringSubmatch(string(data), -1)
for _, match := range matches {
- // Convert CamelCase function name part to lowercase command name.
- // e.g., newAuthLoginCmd -> "authlogin" but we also store original.
- // The convention is that the command name is the lowercase version
- // of the captured group, e.g., AuthLogin -> "auth login" or "auth-login".
- // We'll normalize to lowercase for matching against help output.
- cmdName := strings.ToLower(match[1])
+ // Convert CamelCase function name to kebab-case command name.
+ // e.g., newApiGetCategoryCmd -> "api-get-category"
+ // This matches cobra's command naming convention in help output.
+ cmdName := camelToKebab(match[1])
definedCmds[cmdName] = struct{}{}
}
}
@@ -1020,7 +1018,7 @@ func checkCommandTree(dir string) CommandTreeResult {
}
for cmdName := range definedCmds {
- if strings.Contains(helpLower, cmdName) {
+ if commandFoundInHelp(helpLower, cmdName) {
result.Registered++
} else {
result.Unregistered = append(result.Unregistered, cmdName)
@@ -1030,6 +1028,27 @@ func checkCommandTree(dir string) CommandTreeResult {
return result
}
+// commandFoundInHelp checks if a command name (kebab-case) appears in help output.
+// For compound names like "api-get-category", the function name encodes the parent-child
+// hierarchy (newApiGetCategoryCmd → api-get-category), but help output shows them
+// separately: "api" at top level and "get-category" in the api subcommand help.
+// This function checks the full name first, then progressively strips leading segments
+// to match subcommand names in their parent's help output.
+func commandFoundInHelp(helpLower, cmdName string) bool {
+ if strings.Contains(helpLower, cmdName) {
+ return true
+ }
+ // Try suffix matching: "api-get-category" → "get-category" → "category"
+ parts := strings.SplitN(cmdName, "-", 2)
+ for len(parts) == 2 && parts[1] != "" {
+ if strings.Contains(helpLower, parts[1]) {
+ return true
+ }
+ parts = strings.SplitN(parts[1], "-", 2)
+ }
+ return false
+}
+
// extractCommandNames extracts command names from cobra --help output.
// It looks for the "Available Commands:" section.
func extractCommandNames(helpOutput string) []string {
diff --git a/internal/pipeline/dogfood_test.go b/internal/pipeline/dogfood_test.go
index f6bb15c3..1311f8a2 100644
--- a/internal/pipeline/dogfood_test.go
+++ b/internal/pipeline/dogfood_test.go
@@ -347,6 +347,31 @@ func newBarCmd() {}
assert.Empty(t, result.Unregistered)
}
+func TestCheckCommandTree_KebabConversion(t *testing.T) {
+ dir := t.TempDir()
+ cliDir := filepath.Join(dir, "internal", "cli")
+ require.NoError(t, os.MkdirAll(cliDir, 0o755))
+
+ // Define subcommands with CamelCase function names
+ writeTestFile(t, filepath.Join(cliDir, "api.go"), `package cli
+func newApiGetCategoryCmd() {}
+func newApiListTeamsCmd() {}
+`)
+ writeTestFile(t, filepath.Join(cliDir, "auth.go"), `package cli
+func newAuthLoginCmd() {}
+`)
+ writeTestFile(t, filepath.Join(cliDir, "sync.go"), `package cli
+func newSyncCmd() {}
+`)
+
+ result := checkCommandTree(dir)
+ // Should find 4 defined commands
+ assert.Equal(t, 4, result.Defined)
+ // Without a buildable binary, all treated as registered
+ assert.Equal(t, 4, result.Registered)
+ assert.Empty(t, result.Unregistered)
+}
+
func TestCheckConfigConsistency(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "internal", "cli"), 0o755))
diff --git a/internal/pipeline/runtime.go b/internal/pipeline/runtime.go
index 9101257b..da14e88b 100644
--- a/internal/pipeline/runtime.go
+++ b/internal/pipeline/runtime.go
@@ -452,6 +452,18 @@ func syntheticArgValue(name string) string {
return "/mock/path"
case "query", "search", "name":
return "mock-query"
+ case "type", "entity-type", "entity", "kind":
+ return "collection"
+ case "resource", "resource-type":
+ return "items"
+ case "format", "output-format":
+ return "json"
+ case "category", "slug":
+ return "general"
+ case "action", "command", "operation":
+ return "list"
+ case "status", "state":
+ return "active"
default:
return "mock-value"
}
diff --git a/internal/pipeline/runtime_test.go b/internal/pipeline/runtime_test.go
index 66fc8817..1805143f 100644
--- a/internal/pipeline/runtime_test.go
+++ b/internal/pipeline/runtime_test.go
@@ -218,3 +218,29 @@ func main() {}
assert.Equal(t, filepath.Join(dir, "sample-pp-cli-2"), binaryPath)
assert.FileExists(t, binaryPath)
}
+
+func TestSyntheticArgValue(t *testing.T) {
+ tests := []struct {
+ name string
+ expected string
+ }{
+ {"type", "collection"},
+ {"entity-type", "collection"},
+ {"resource", "items"},
+ {"format", "json"},
+ {"category", "general"},
+ {"action", "list"},
+ {"status", "active"},
+ // Existing mappings still work
+ {"query", "mock-query"},
+ {"id", "12345"},
+ {"region", "mock-city"},
+ // Unknown falls back to mock-value
+ {"unknown-placeholder", "mock-value"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.expected, syntheticArgValue(tt.name))
+ })
+ }
+}
← 5026f516 fix(skills): publish skill supports fork-based PRs for exter
·
back to Cli Printing Press
·
docs(cli): mark scorer/pagination/proxy-routes plan as compl 25e059c3 →