fix(codegen/digitalocean): address every generated model by a real identifier (swamp-club #2834) #461

Merged
stack72 merged 5 commits from 2834-2 into main 2026-10-06 18:31:15 +00:00
Owner

Fixes swamp-club #2834.

Problem

update and sync on generated DigitalOcean models read the resource ID from stored state using an identifier that codegen derived by stripping underscores from the path parameter (access_key became accesskey). Where that field did not exist and the response had no id, the models requested /v2/.../undefined, and sync recorded live resources as not_found.

The fix is in codegen/digitalocean/; everything under model/digitalocean/ is regenerated output.

Changes, grouped by cause

1. Identifier resolution (pipeline.ts): the identifier is now matched against the GET response fields: overrides, IDENTIFIER_MAP, an exact match, a match ignoring case and underscores, then id. This fixes seven models:

  • space_key (access_key), the reported bug
  • reserved_ipv6 (ip)
  • byoip_prefix (uuid)
  • function_namespace (namespace)
  • action_gateway_mcp_server (serverRef)
  • action_gateway_output_view (view_id)
  • monitoring_sink (see 3)

Ten more models that only worked because of the existing.id fallback (load_balancer, kubernetes_cluster, ssh_key and others) now resolve to id directly. Their output changes only cosmetically.

2. Generation guard: a model whose identifier is neither a response field nor id is reported as a generation error. Its previous file is kept, and DigitalOcean generation now exits 2, which the nightly job already expects. The guard found reserved_ipv6, which my triage audit had missed.

3. Monitoring sink (IDENTIFIER_FROM_ARGS): the API never returns the sink UUID, according to the spec. get persists its argument, and sync/update carry it over. After create, sync fails with "run get with the resource ID first".

4. get/sync only with a GET-by-id endpoint: action_gateway_session loses get and sync. Neither ever worked: the API has no single-session read endpoint.

5. Clear errors instead of /undefined: update and sync in every model throw a named error when stored state has no identifier. This is why about 50 models change uniformly.

6. Create-response siblings (createEnveloped): with an explicit envelope key, the session keeps mcpUrl and the connection keeps authorization. These are a create-time snapshot that the first sync replaces. The dedicated_inference token secret is deliberately still not stored.

7. checkExists: it reads by name only when the name is the identifier, so byoip_prefix keeps its list-filter check.

8. Stable get/delete arguments: byoip_prefix and reserved_ipv6 keep taking id.

9. Orphan pruning for DigitalOcean, Hetzner and Tailscale (AWS, GCP, Cloudflare and Vercel already prune). This removes the stale security_secret.ts, which calls an endpoint no longer in the spec and was absent from the manifest. Pruning is skipped when generation reports errors, and runs after files are written. Hetzner and Tailscale have no diff.

Separate commit: 3d47c85 contains unrelated upstream spec drift (insight_notification_channel) from the baseline regeneration, isolated so the fix diff stays reviewable.

Verification

  • Codegen: 466 tests pass. New: pipeline_test.ts, generatedModel_test.ts (runs the real pipeline on a minimal spec and drives the generated models against a stubbed fetch), and commands/generate_test.ts. Snapshots were updated for the identifier changes only.
  • The original repro now hits GET /v2/spaces/keys/<access_key>, and sync keeps the key's state.
  • Regeneration is idempotent (a second run produces no diff), with one CalVer bump (2026.10.06.2).
  • Real swamp CLI check: data that doesn't match the resource schema logs a warning but is still stored, so the now-required identifiers cannot fail a write.
  • Attested pre-PR verification on 67ce99e: verify-build 14/14; code-review and adversarial-review pass. Three review rounds of non-blocking findings were fixed in d301acf, 63403f8 and 67ce99e.

Assumptions and known limits

  • Spec-based, not checked against the live API:
    • function_namespace uses namespace, based on the spec's fn-xxxx example.
    • reserved_ipv6 uses ip, inferred from the path parameter.
    • The monitoring sink returning no ID anywhere.
  • Stored not_found markers written by earlier versions under a stripped key (for example { lbid: X }) no longer resolve. A re-sync of such a record stops with "run get with the resource ID first" instead of re-requesting the 404.
  • The connection's authorization (connect_url, verification_code) is stored unvaulted until the first sync.

🤖 Generated with Claude Code

Fixes swamp-club #2834. ## Problem `update` and `sync` on generated DigitalOcean models read the resource ID from stored state using an identifier that codegen derived by stripping underscores from the path parameter (`access_key` became `accesskey`). Where that field did not exist and the response had no `id`, the models requested `/v2/.../undefined`, and `sync` recorded live resources as `not_found`. The fix is in `codegen/digitalocean/`; everything under `model/digitalocean/` is regenerated output. ## Changes, grouped by cause **1. Identifier resolution** (`pipeline.ts`): the identifier is now matched against the GET response fields: overrides, `IDENTIFIER_MAP`, an exact match, a match ignoring case and underscores, then `id`. This fixes seven models: - `space_key` (`access_key`), the reported bug - `reserved_ipv6` (`ip`) - `byoip_prefix` (`uuid`) - `function_namespace` (`namespace`) - `action_gateway_mcp_server` (`serverRef`) - `action_gateway_output_view` (`view_id`) - `monitoring_sink` (see 3) Ten more models that only worked because of the `existing.id` fallback (`load_balancer`, `kubernetes_cluster`, `ssh_key` and others) now resolve to `id` directly. Their output changes only cosmetically. **2. Generation guard:** a model whose identifier is neither a response field nor `id` is reported as a generation error. Its previous file is kept, and DigitalOcean generation now exits 2, which the nightly job already expects. The guard found `reserved_ipv6`, which my triage audit had missed. **3. Monitoring sink** (`IDENTIFIER_FROM_ARGS`): the API never returns the sink UUID, according to the spec. `get` persists its argument, and `sync`/`update` carry it over. After `create`, `sync` fails with "run get with the resource ID first". **4. `get`/`sync` only with a GET-by-id endpoint:** `action_gateway_session` loses `get` and `sync`. Neither ever worked: the API has no single-session read endpoint. **5. Clear errors instead of `/undefined`:** `update` and `sync` in every model throw a named error when stored state has no identifier. This is why about 50 models change uniformly. **6. Create-response siblings** (`createEnveloped`): with an explicit envelope key, the session keeps `mcpUrl` and the connection keeps `authorization`. These are a create-time snapshot that the first `sync` replaces. The `dedicated_inference` token secret is deliberately still not stored. **7. `checkExists`:** it reads by name only when the name is the identifier, so `byoip_prefix` keeps its list-filter check. **8. Stable `get`/`delete` arguments:** `byoip_prefix` and `reserved_ipv6` keep taking `id`. **9. Orphan pruning** for DigitalOcean, Hetzner and Tailscale (AWS, GCP, Cloudflare and Vercel already prune). This removes the stale `security_secret.ts`, which calls an endpoint no longer in the spec and was absent from the manifest. Pruning is skipped when generation reports errors, and runs after files are written. Hetzner and Tailscale have no diff. **Separate commit:** `3d47c85` contains unrelated upstream spec drift (`insight_notification_channel`) from the baseline regeneration, isolated so the fix diff stays reviewable. ## Verification - Codegen: 466 tests pass. New: `pipeline_test.ts`, `generatedModel_test.ts` (runs the real pipeline on a minimal spec and drives the generated models against a stubbed fetch), and `commands/generate_test.ts`. Snapshots were updated for the identifier changes only. - The original repro now hits `GET /v2/spaces/keys/<access_key>`, and `sync` keeps the key's state. - Regeneration is idempotent (a second run produces no diff), with one CalVer bump (`2026.10.06.2`). - Real swamp CLI check: data that doesn't match the resource schema logs a warning but is still stored, so the now-required identifiers cannot fail a write. - Attested pre-PR verification on `67ce99e`: verify-build 14/14; code-review and adversarial-review pass. Three review rounds of non-blocking findings were fixed in `d301acf`, `63403f8` and `67ce99e`. ## Assumptions and known limits - **Spec-based, not checked against the live API:** - `function_namespace` uses `namespace`, based on the spec's `fn-xxxx` example. - `reserved_ipv6` uses `ip`, inferred from the path parameter. - The monitoring sink returning no ID anywhere. - Stored `not_found` markers written by earlier versions under a stripped key (for example `{ lbid: X }`) no longer resolve. A re-`sync` of such a record stops with "run get with the resource ID first" instead of re-requesting the 404. - The connection's `authorization` (`connect_url`, `verification_code`) is stored unvaulted until the first `sync`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Baseline regeneration with unchanged codegen before the swamp-club #2834
fix, so upstream spec drift (insight_notification_channel) stays out of
the fix's diff.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
update and sync looked the resource up by an identifier the pipeline
derived by stripping underscores from the path param (access_key ->
accesskey). Where that field did not exist and the response had no id,
they requested /v2/.../undefined and sync recorded live resources as
not_found. Seven models were affected: space_key, reserved_ipv6,
byoip_prefix, function_namespace, action_gateway_mcp_server,
action_gateway_output_view and monitoring_sink.

- Resolve the identifier against the GET response fields (exact, then
  case- and underscore-insensitive, then id), with IDENTIFIER_MAP
  entries for spec-documented fields; fail generation per model when
  it resolves to nothing.
- IDENTIFIER_FROM_ARGS: the monitoring sink's API never returns its
  UUID, so get persists its argument and sync carries it over.
- Emit get and sync only when a GET-by-id exists; the action gateway
  session never had one.
- update and sync throw a clear error when stored state has no
  identifier instead of requesting /undefined.
- Keep create-response siblings the caller needs (session mcpUrl,
  connection authorization) via createEnveloped; the dedicated
  inference token secret stays unstored.
- checkExists reads by name only when the name is the identifier.
- Keep get/delete argument names stable (byoip_prefix, reserved_ipv6).
- Prune model files generation no longer produces for DigitalOcean,
  Hetzner and Tailscale, removing the stale security_secret.ts; exit 2
  on DigitalOcean generation errors, as the nightly job expects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review findings on the previous commit:

- Droplet's create response is a oneOf ({ droplet } or { droplets }),
  which flattening merged into a two-key "envelope". Skip oneOf/anyOf
  responses, and require the resource key's object to share fields
  with the GET resource.
- sync carried the connection's create-time authorization (pending
  status, verification code) forward forever. Siblings are now a
  create-time snapshot that the first sync replaces with live state;
  only an identifier persisted from get arguments is carried over.
- Fix the carry-over line's indentation inside sync, and leave
  *_test.ts files alone when pruning orphan models.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Second-round review findings:

- A flat create response with a nested object sharing a field name
  ({ id, name, region: { name } }) could pick region as the envelope
  key. The key must now carry the resource's identifier, and an
  identifier at the top level marks the response as flat.
- Run the identifier guard for resources with update but no GET-by-id,
  whose update reads the identifier from stored state.
- Remove the temp dirs the end-to-end tests create.

No generated output changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fix(codegen/digitalocean): live-fill only with a GET-by-id; prune after writes (swamp-club #2834)
All checks were successful
CI / Review Integrity (pull_request) Successful in 1m24s
CI / Validate Attestation (pull_request) Successful in 2m12s
67ce99ec89
Third-round review findings:

- A PUT update's live-fill reads the resource by ID, so emit it only
  when a GET-by-id exists; otherwise the generated model could call a
  read it no longer imports.
- Prune orphan models after the new files are written, so a failed
  write never leaves a provider with files deleted and nothing written.
- Describe identifier-keyed envelope detection accurately in the
  DigitalOcean design doc.

No generated output changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
stack72 deleted branch 2834-2 2026-10-06 18:31:16 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
swamp-club/swamp-extensions!461
No description provided.