basecamp / basecamp/basecamp-sdk

Thirteen operations report a wrong resourceType, and the drift gate locks them in

Open
#640 1 comment 0 reactions 0 assignees View on GitHub
bug
Dominant language
Go
Stars
49
Forks
12
Avg merge
20h 47m
Merged PRs (30d)
89

Description

## resourceType: ten generated ops emit the generic `"resource"`, and TypeScript mangles three `-ies` plurals its peers get right

### Summary

`extractResourceType` has two unguarded exits that return the literal `"resource"`. On `main@c95d81ceb` this affects **10 of 241 operations**, identically in TypeScript, Kotlin, and Swift. Separately, TypeScript's naive singularizer regex emits **3 values that disagree with Kotlin and Swift** for the same operationId.

`resourceType` is observability-only (OTel span attribute / slog field), so nothing functionally breaks. The cost is dashboard and alert grouping.

### The ten generic operations

`RemoveAccountLogo`, `DestroyGaugeNeedle`, `ToggleGauge`, `PrioritizeAssignment`, `DeprioritizeAssignment`, `ReorderUpNext`, `MarkAsRead`, `Subscribe`, `Unsubscribe`, `Search`

Two causes:
- **Seven** have an operationId whose verb prefix is absent from `VERB_PATTERNS` (`Remove`, `Destroy`, `Toggle`, `Prioritize`, `Deprioritize`, `Reorder`, `MarkAsRead`) and fall through to the final `return "resource"` (`typescript/scripts/generate-services.ts:695`).
- **Three** (`Subscribe`, `Unsubscribe`, `Search`) match a prefix but leave an empty remainder, hitting `if (!remainder) return "resource"` (`:687`).

### Why this is a defect and not a naming judgment call

The repo already adopted the rule, in `RESOURCE_TYPE_OVERRIDES` (`typescript/scripts/generate-services.ts:315-318`):

```
// "Destroy" is not a verb pattern, so inference falls through to the generic
// "resource" and would split this delete away from the get/update siblings
// that report "timesheet_entry".
DestroyTimesheetEntry: "timesheet_entry",
```

`DestroyGaugeNeedle` is the same shape and was not fixed. In `gauges.ts` the split is visible in one file:

```
GetGaugeNeedle -> "gauge_needle"
UpdateGaugeNeedle -> "gauge_needle"
ListGaugeNeedles -> "gauge_needle"
CreateGaugeNeedle -> "gauge_needle"
DestroyGaugeNeedle -> "resource" <-- split from its own family
ToggleGauge -> "resource"
```

Same in `subscriptions.ts` (`Subscribe`/`Unsubscribe` split from `GetSubscription`/`UpdateSubscription` = `"subscription"`).

Worse than the split: the ten also **falsely merge**. A dashboard grouped by `basecamp.resource_type` puts gauge deletes, notification reads, subscription changes, account-logo removal, assignment reordering, and search into a single `"resource"` bucket spanning six unrelated services.

### The cross-SDK divergence (strictly worse)

TypeScript: `snakeCase.replace(/([^s])s$/, "$1")`. Kotlin and Swift implement real `ies -> y` rules.

| operationId | TypeScript | Kotlin | Swift |
|---|---|---|---|
| `ListClientReplies` | `client_replie` | `client_reply` | `client_reply` |
| `ListForwardReplies` | `forward_replie` | `forward_reply` | `forward_reply` |
| `ListScheduleEntries` | `schedule_entrie` | `schedule_entry` | `schedule_entry` |

The Swift generator's docstring names the expected output outright (`swift/Sources/BasecampGenerator/Utilities.swift:89-91`): `"schedule_entries" -> "schedule_entry"`, `"client_replies" -> "client_reply"`. So the TypeScript values are wrong against the project's own stated intent, not a house-style difference.

### Scope

- **Affected:** TypeScript, Kotlin, Swift (all verb-table-derived).
- **Not affected:** Go — `pkg/basecamp` is hand-written and assigns real nouns (`gauges.go:436` `"gauge_needle"`, `subscriptions.go:74` `"subscription"`, `search.go:182` `"search"`).
- **N/A:** Python and Ruby never populate the field at all — `python/src/basecamp/hooks.py:12` defaults it to `None`, `ruby/.../base_service.rb:43` to `nil`, for every operation.

### Why `make check` does not catch it

`git grep -l 'resourceType\|resource_type' HEAD -- scripts/ .github/` returns exit 1 — no matches. None of the targets in `check:` (`Makefile:1147`) inspects the field, and `conformance/` has zero references.

The three regenerate-and-diff freshness gates make this worse, not better: they prove each emitter reproduces its committed bytes, so they **lock the wrong values in**. `scripts/check-typescript-service-drift.sh` passes green today (verified, exit 0) with all thirteen wrong values present. `scripts/check-retry-metadata-parity.py:8-12` draws exactly this distinction in its own docstring.

### Suggested fix

1. Add the missing verbs to `VERB_PATTERNS`, or add the ten to `RESOURCE_TYPE_OVERRIDES` — the same one-line-per-op mechanism already used for `DestroyTimesheetEntry`. Do it in all three generators.
2. Fix the TypeScript singularizer to match Kotlin/Swift (`ies -> y`, `sses`/`ss` guards).
3. Add a value-parity gate modeled on `scripts/check-retry-metadata-parity.py` (~60 lines): parse `(operationId, resourceType)` from the three committed artifact trees and assert (a) all three SDKs agree, (b) no value is the literal `"resource"` outside a commented allowlist. Go cannot join this gate — its `OperationInfo.Operation` uses short names (`"RemoveLogo"`, `"DestroyNeedle"`, `"Metadata"`), not canonical operationIds, so there is no join key.

Note that (1) and (2) change emitted telemetry strings, which will re-bucket existing dashboards — ship in a minor release, not a patch.

Contributor guide

Open the contributing guide

Research direction

Start in typescript/scripts/generate-services.ts at VERB_PATTERNS, RESOURCE_TYPE_OVERRIDES, and the singularizer, then compare the corresponding Kotlin and Swift generators, including swift/Sources/BasecampGenerator/Utilities.swift. Run scripts/check-typescript-service-drift.sh and use scripts/check-retry-metadata-parity.py as the model for the cross-SDK gate. Done means the affected generated artifacts agree across TypeScript, Kotlin, and Swift and generic resource values are absent except for an explicit allowlist.

Written by the indexing model from the issue text.

Assessment

Tech stack
kotlin, swift, typescript
Domain
observability, testing, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.