Skip to content

Generator fixes: which scorer natural-language ranking + store composite resources PK#3476

Open
roboulos wants to merge 6 commits into
mvanhorn:mainfrom
roboulos:which-scorer-natural-language-ranking
Open

Generator fixes: which scorer natural-language ranking + store composite resources PK#3476
roboulos wants to merge 6 commits into
mvanhorn:mainfrom
roboulos:which-scorer-natural-language-ranking

Conversation

@roboulos

@roboulos roboulos commented Jul 6, 2026

Copy link
Copy Markdown

Two generator-template defects found live through Snappy OS's connector substrate, both fixed at the template so every generated CLI heals on regen.

1. which scorer cannot rank obvious natural-language capability asks (which.go.tmpl)

github-pp-cli which "my authenticated user repositories" never surfaced the indexed user repos-list-for-authenticated (not even in the top 30): hyphenated command leaves were matched as single exact tokens, so capability words inside a leaf were unmatchable and every command in a group tied on the group token with source order deciding.

Fixes: sub-token splitting (spaces/hyphens/underscores/slashes) for both command paths and queries, one-sided prefix normalization (repositories ~ repos), capped description token credit (uncapped credit let token-soup descriptions outrank the precise command), possessive aliasing (my/mine/me/current boosts authenticated-scoped commands), a write-verb penalty so read-shaped asks never rank destructive commands first on ties, and a specificity tie-break (fewest capability sub-tokens wins).

Proof on the rebuilt github CLI: "my authenticated user repositories", "list repositories for authenticated user", and "repos-list-for-authenticated" all rank user repos-list-for-authenticated first; coingecko "top coins by market cap" -> coins markets unregressed. Known limit: bare "my repos" still favors group-token matches.

2. store v1 PK silently merges rows across resource types (store.go.tmpl)

resources.id was PRIMARY KEY alone, so a markets upsert for id bitcoin merged into the coins row and a ping health-check row ate a real coin named Ping - rows vanished while sync_summary reported success. Fixed: composite PRIMARY KEY (resource_type, id) + matching conflict target, StoreSchemaVersion 2 with an in-place rebuild for v1 databases, and type-namespaced generic FTS rowids.

Both verified against live CLIs (github, coingecko) and the generator test suite is green.

🤖 Generated with Claude Code

roboulos and others added 6 commits May 10, 2026 23:12
Downstream snappy-os runs a skill-provenance-required lint (Phase E.7)
that mandates `created_by: user|agent` frontmatter on every skill it
manages. The 9 PP skills are symlinked into snappy-os
(state/skills/printing-press*/SKILL.md -> here), so the lint reads
these files directly. Phase D.5d (2026-05-10) removed the upstream-
managed exemption, expecting the upstream files to carry the field.

Adding `created_by: user` to mark these as human-authored (so the
snappy curator's auto-archive-stale-agent-skills rule never touches
them) and clear the lint at 230/230.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…anguage discovery

The snappy-os SearchSkill index (state/lib/harness/skill-search.ts) reads
this `description:` field as the sole input for token-overlap scoring. The
prior wording leaned on "Generate a ship-ready CLI ... shipcheck loop"
vocabulary that no user types in chat. Live test on 2026-05-11 with the
natural prompt "set up a new integration with httpbin.org" returned zero
points for printing-press across three SearchSkill queries the model tried
("integration setup API connector", "connector configuration webhook",
"httpbin test API"). The model fell through to AskUserQuestion and never
reached InvokeSkill.

Broaden the description with the actual phrases users say. Retest with the
same prompt now ranks printing-press at mvanhorn#2 for "integration setup API
connector" and "wrap api httpbin". Token scoring details in
/Users/robertboulos/projects/snappy-os/state/log/pp-from-chat-live-test-2026-05-11.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…l catalog entry)

Add internal/googlediscovery package that converts Google Discovery REST
descriptions (googleapis.com/discovery/v1/apis) into the internal APISpec
the generator consumes. Wire IsDiscovery detection into the generate
command's spec-format dispatch alongside the existing OpenAPI and GraphQL
branches.

Key mapping: Discovery resources.methods -> OpenAPI paths, Discovery
parameters (location: path) -> positional Params (sorted first so
args[i] indices are stable), Discovery schemas -> TypeDef-compatible body
params, Discovery auth.oauth2 -> bearer_token AuthConfig.

Handles multi-level nesting via flattenInto: sub-sub-resources are
promoted as sibling sub-resources with compound names (settings-delegates)
so the generator's single-level SubResource template works without changes.

Verified: printing-press generate --spec gmail-discovery.json passes all
gates (go mod tidy, govulncheck, go vet, go build, --help, version,
doctor). Dry-run produces correct URL: GET .../gmail/v1/users/me/messages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Every connector generation failed the govulncheck gate with exit 3: the
generated module resolved golang.org/x/net at 0.43.0 (GO-2026-5026,
reachable via cliutil.ProbeReachable) and go1.26.3 carries two reachable
stdlib vulns (GO-2026-5039 net/textproto, GO-2026-5037 crypto/x509),
both fixed in go1.26.4. The template now pins x/net v0.55.0
unconditionally (it was gated behind HasHTMLExtraction while cliutil
pulls it transitively for every connector) and declares toolchain
go1.26.4 so generated modules auto-fetch the patched Go. Probe spec
(apis.guru google.com/v3) now generates, validates, and bundles clean.
Five defects fixed in the generated scorer:
- hyphenated command leaves were single exact tokens, so 'authenticated' or
  'repositories' could never match repos-list-for-authenticated and every
  command in a group tied on the group token with index order deciding
- no prefix normalization (repositories ~ repos, market ~ markets)
- description token credit was uncapped, letting token-soup descriptions
  outrank the precise command path (now saturates at 3)
- no possessive aliasing: 'my/mine/me/current' is API-speak for the
  authenticated caller; authenticated-scoped commands now outrank generic
  listings for possessive asks
- ties broke by index order; now by command specificity (fewest capability
  sub-tokens), and write-verb commands are penalized for read-shaped asks so
  a neutral request can never rank a destructive command first

Proof (github library rebuilt from this scorer): 'my authenticated user
repositories', 'list repositories for authenticated user', and
'repos-list-for-authenticated' all rank user repos-list-for-authenticated
first; coingecko 'top coins by market cap' unregressed (coins markets first).
Known limit: bare 'my repos' still favors group-token matches - tracked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The v1 single-column PK silently merged rows across resource types on upsert:
a markets sync for id 'bitcoin' overwrote the coins row (kept resource_type
coins, swapped the data), and a ping health-check row ate a real coin named
'ping'. Fetched rows vanished while sync_summary reported success - the
worst kind of data loss, invisible at every layer above the store.

- resources DDL: PRIMARY KEY (resource_type, id); upsert conflict target
  matches
- StoreSchemaVersion 2 with an in-place rebuild for v1 databases (create v2
  shape, copy with INSERT OR IGNORE, swap, reindex; generic FTS emptied and
  repopulated on next sync)
- generic FTS rowids namespaced by resource type so two types sharing an id
  cannot cross-delete each other's index entries

Found live on the coingecko CLI (markets rows fetched=100 stored=0; fresh-db
control proved the write path clean when no collision exists). Generator
test suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mergify

mergify Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 2 of 2 protections blocking · waiting on 👀 reviews, 🤖 CI and 🙋 you

Protection Waiting on
🔴 require-ready-label-and-ci 👀 reviews, 🤖 CI and 🙋 you
🔴 🚦 Auto-queue 🙋 you

🔴 require-ready-label-and-ci

Waiting for

  • #review-threads-unresolved = 0
  • check-success = build-and-test
  • check-success = generated-test
  • check-success = go-lint
  • check-success = golden
  • check-success = pr-title
  • check-success = test
  • any of:
    • label = ready-to-merge
    • title ~= ^chore\(main\): release
This rule is failing.
  • #review-threads-unresolved = 0
  • check-success = build-and-test
  • check-success = generated-test
  • check-success = go-lint
  • check-success = golden
  • check-success = pr-title
  • check-success = test
  • any of:
    • label = ready-to-merge
    • all of:
      • head = release-please--branches--main
      • title ~= ^chore\(main\): release
  • #changes-requested-reviews-by = 0
  • any of:
    • -files ~= ^(\.github/workflows/|\.github/scripts/|scripts/|\.github/CODEOWNERS$)
    • approved-reviews-by = mvanhorn
    • approved-reviews-by = tmchow
    • author = mvanhorn
    • author = tmchow
  • any of:
    • check-success = Greptile Review
    • check-neutral = Greptile Review
    • check-skipped = Greptile Review
    • head ~= ^mergify/merge-queue/
    • label = queued
    • all of:
      • head = release-please--branches--main
      • title ~= ^chore\(main\): release

🔴 🚦 Auto-queue

Waiting for

  • label = ready-to-merge
This rule is failing.

When all merge protections are satisfied and these conditions match, this pull request will be queued automatically.

  • label = ready-to-merge

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes generated CLI ranking and store keying while adding Google Discovery support. The main changes are:

  • Added a Gmail catalog entry and a Google Discovery parser.
  • Routed Discovery specs through printing-press generate.
  • Changed generated stores to use (resource_type, id) for generic resources.
  • Updated generated which scoring for sub-token and natural-language matches.
  • Bumped generated and repo Go module dependencies.

Confidence Score: 4/5

The Discovery generation path and generic store search can produce incorrect generated CLI behavior.

  • Discovery specs can lose the service base path for methods without flatPath.
  • Discovery path arguments can be bound in alphabetical order instead of API path order.
  • Generic store search can mix rows from different resource types that share an id.
  • The which changes have smaller ranking issues that can affect command selection.

internal/googlediscovery/parser.go, internal/generator/templates/store.go.tmpl, internal/generator/templates/which.go.tmpl

Important Files Changed

Filename Overview
internal/googlediscovery/parser.go Adds Discovery-to-APISpec conversion, with issues in service base path handling and path parameter order.
internal/generator/templates/store.go.tmpl Updates generated store schema and migration to composite generic resource keys, but generic FTS search can still join rows by bare id.
internal/generator/templates/which.go.tmpl Improves generated which matching, but some scoring rules can rank unrelated or less useful commands.
internal/cli/root.go Adds Discovery detection to the generate command dispatch path.
catalog/gmail.yaml Adds Gmail as an official Google Discovery catalog entry.
internal/generator/templates/go.mod.tmpl Updates generated module dependency defaults and adds a Go toolchain line.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Google Discovery JSON] --> B[googlediscovery.Parse]
  B --> C[Generated APISpec]
  C --> D[Generated CLI]
  D --> E[API requests]
  F[v1 SQLite store] --> G[v2 migration]
  G --> H[resources keyed by resource_type and id]
  H --> I[Generic FTS index]
  I --> J[CLI and MCP search]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
  A[Google Discovery JSON] --> B[googlediscovery.Parse]
  B --> C[Generated APISpec]
  C --> D[Generated CLI]
  D --> E[API requests]
  F[v1 SQLite store] --> G[v2 migration]
  G --> H[resources keyed by resource_type and id]
  H --> I[Generic FTS index]
  I --> J[CLI and MCP search]
Loading

Comments Outside Diff (1)

  1. internal/generator/templates/store.go.tmpl, line 539-548 (link)

    P1 FTS Join Uses Bare Id

    The FTS rowid is now namespaced by resource type, but the FTS table still stores the bare id and generic search joins resources by r.id = f.id. After two resource types legitimately share an id, one matching FTS row can join to every resource row with that id, so search and MCP search can return data from the wrong resource type.

    Fix in Codex Fix in Claude Code Fix in Cursor Fix in Conductor

Fix All in Codex Fix All in Claude Code Fix All in Cursor Fix All in Conductor

Reviews (1): Last reviewed commit: "store schema v2: composite resources PRI..." | Re-trigger Greptile

Comment on lines +119 to +124
baseURL := doc.RootURL
if baseURL == "" {
baseURL = doc.BaseURL
}
// Strip trailing slash; generator adds its own separators.
baseURL = strings.TrimRight(baseURL, "/")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Base Path Is Dropped

Discovery path values are relative to the service basePath, but this converter stores only rootUrl or baseUrl. When a valid method has no flatPath, the generated CLI calls https://www.googleapis.com/{relative-path} instead of the service path such as /gmail/v1/{relative-path}, so requests go to the wrong endpoint and fail.

Fix in Codex Fix in Claude Code Fix in Cursor Fix in Conductor

Comment on lines +273 to +283
var pathParams, queryParams []spec.Param
for paramName, param := range method.Parameters {
p := convertParam(paramName, &param)
if param.Location == "path" {
pathParams = append(pathParams, p)
} else {
queryParams = append(queryParams, p)
}
}
sort.Slice(pathParams, func(i, j int) bool { return pathParams[i].Name < pathParams[j].Name })
sort.Slice(queryParams, func(i, j int) bool { return queryParams[i].Name < queryParams[j].Name })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Path Arguments Reordered

Discovery provides parameterOrder for positional path parameters, but this code sorts path params alphabetically. Generated commands bind positional args from this order before replacing {name} placeholders, so any method whose path order differs from alphabetical order sends user arguments to the wrong resource path.

Fix in Codex Fix in Claude Code Fix in Cursor Fix in Conductor

Comment on lines +241 to +243
if len(a) >= 4 && len(b) >= 4 && (strings.HasPrefix(a, b) || strings.HasPrefix(b, a)) {
return true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Prefix Match Crosses Resources

This treats any two tokens of length four or more as equal when one is a prefix of the other. A query token like user can score an unrelated command token such as usersettings, or list can score listeners, which can rank the wrong command for an agent-facing which lookup.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code Fix in Cursor Fix in Conductor

Comment on lines +205 to +223
if score > 0 {
unmatched := 0
for _, ct := range cmdTokens {
hit := false
for _, qt := range qTokens {
if whichTokenMatch(qt, ct) {
hit = true
break
}
}
if !hit {
unmatched++
}
}
if unmatched > 3 {
unmatched = 3
}
score -= unmatched
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Specificity Erases Nested Matches

The specificity penalty counts every unmatched command sub-token, including parent resource and operation tokens. For a query like tasks, a matching nested command such as projects tasks list-project gains credit for tasks and then loses the same amount to unrelated path tokens, so a valid lookup can be dropped as a zero-score match or lose to a less relevant command.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code Fix in Cursor Fix in Conductor

Comment on lines +255 to +262
var whichWriteVerbs = map[string]bool{
"delete": true, "remove": true, "update": true, "create": true, "set": true,
"add": true, "replace": true, "rename": true, "transfer": true, "merge": true,
"lock": true, "unlock": true, "star": true, "unstar": true, "follow": true,
"unfollow": true, "block": true, "unblock": true, "mute": true, "archive": true,
"unarchive": true, "cancel": true, "send": true, "upload": true, "subscribe": true,
"unsubscribe": true, "dismiss": true, "approve": true, "decline": true,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Write Synonyms Look Read-Only

The read-intent penalty only recognizes exact tokens in this map. Queries such as edit project, post message, or modify label do not set queryWrite, so the intended mutating command is penalized and a read/list command can rank above the action the user asked for.

Suggested change
var whichWriteVerbs = map[string]bool{
"delete": true, "remove": true, "update": true, "create": true, "set": true,
"add": true, "replace": true, "rename": true, "transfer": true, "merge": true,
"lock": true, "unlock": true, "star": true, "unstar": true, "follow": true,
"unfollow": true, "block": true, "unblock": true, "mute": true, "archive": true,
"unarchive": true, "cancel": true, "send": true, "upload": true, "subscribe": true,
"unsubscribe": true, "dismiss": true, "approve": true, "decline": true,
}
var whichWriteVerbs = map[string]bool{
"delete": true, "remove": true, "update": true, "create": true, "set": true,
"add": true, "replace": true, "rename": true, "transfer": true, "merge": true,
"lock": true, "unlock": true, "star": true, "unstar": true, "follow": true,
"unfollow": true, "block": true, "unblock": true, "mute": true, "archive": true,
"unarchive": true, "cancel": true, "send": true, "upload": true, "subscribe": true,
"unsubscribe": true, "dismiss": true, "approve": true, "decline": true,
"post": true, "put": true, "write": true, "edit": true, "modify": true,
"publish": true, "share": true, "comment": true, "grant": true, "revoke": true,
}

Fix in Codex Fix in Claude Code Fix in Cursor Fix in Conductor

@roboulos

roboulos commented Jul 6, 2026

Copy link
Copy Markdown
Author

Third generator defect found while validating this PR on a live regeneration: syncResourcePath derivation misses nested resources. The coingecko spec declares /coins/markets, but the generated path map only carries top-level entries, so the markets resource falls back to /markets and 404s. Currently hand-fixed in the library copy (markets -> /coins/markets?vs_currency=usd&order=market_cap_desc); the durable fix is deriving sync paths for nested path items (and their required query params) in the profiler. Validation of THIS PR's store v2 on a real db: user_version migrated 1 -> 2 in place, 18456 coins preserved through the rebuild, 100 market rows now persist under the composite PK (they vanished under v1).

@tmchow

tmchow commented Jul 8, 2026

Copy link
Copy Markdown
Owner

@roboulos please rebase to fix merge conflicts and address code review feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants