BEX-471 feat(json): emit snake_case twins for every camelCase --json key (transition step) - #106
Merged
piyushsarin-sib merged 2 commits intoSep 4, 2026
Conversation
…nsition step) Machine-readable output had two conventions: pass-through commands (app list, app submit, function *) emit the wire's snake_case, while CLI-built objects (create, upload, credentials, install, delete, whoami, the error envelope) emit camelCase. app-config.json settled on snake_case in BEX-470, and scripts should be able to read one spelling everywhere. Renaming outright would break every `jq .appId` in a pipeline with nothing the CLI could migrate, so this is the deprecation step: jsonOutput() now adds a snake_case twin after each camelCase key at the top level of every document (per element for arrays) and inside the error envelope. redirectUri / redirectUris alias to the wire name redirect_uris. Nested objects are left untouched — they are wire records or user data. Nothing is removed; camelCase goes away in the next major. Agent docs and the changeset carry the deprecation note; the seven tests that pinned exact camelCase shapes now pin both spellings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ase-keys Resolve the agent-doc conflicts by taking the base's copies (which add the Brevo Function sections and the three-app-type wording) and re-applying this branch's three edits on top: the snake_case --json key rule, the exit_code / status_code lines in the JSON-errors sections, and the scaffold_skipped mention. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
satyamdev10
approved these changes
Sep 4, 2026
|
piyushsarin-sib
added a commit
that referenced
this pull request
Sep 4, 2026
* feat: add Brevo Function commands with smoke tests Add `brevo function list` and `brevo function get <id>` commands for managing Brevo Functions, plus the "Brevo Function" app type choice in `brevo app create`. - New service (src/services/function.ts) with list, draft list, and get - New command handlers (src/commands/function/list.ts, get.ts) - Register functionCommandGroup in definitions.ts and bin/index.ts - Add Function commands section to root help screen - Add brevo_function template flag for app-config.json rendering - Add Brevo Function app type to interactive create prompt (private only) - Unit tests for service, list command, and get command (23 tests) - Command registration tests in definitions.test.ts (4 tests) - Help formatting tests updated for functionCommandGroup - New smoke suite (scripts/smoke/function.ts) exercising list, list --draft, get, and get-not-found against a real account — opt-in via `yarn smoke --suite=function` Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: gate Brevo Function behind __BREVO_PREVIEW__ and add fn alias - Add `brevo-function-type` to FEATURE_STAGE as a preview feature - Move function group definition to preview-definitions.ts so esbuild can eliminate it from published builds - Gate the Brevo Function choice in `app create` behind __BREVO_PREVIEW__ - Gate the Function commands section in help.ts behind __BREVO_PREVIEW__ - Add `aliases` support to SubcommandGroupDefinition so `brevo fn list` and `brevo fn get` work as shortcuts - Update tests for the conditional export and gated app-type prompt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add brevo function activate, deactivate and delete commands Add three management commands to the Brevo Function group: - `brevo function activate <id>` — PATCH with is_active: true - `brevo function deactivate <id>` — PATCH with is_active: false - `brevo function delete <id>` — DELETE with --force to skip confirmation All three are gated behind __BREVO_PREVIEW__ alongside the existing list and get commands. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add brevo fn init command with AI generation, template flow, and SSE error handling - Add `brevo fn init` command with two creation flows: AI generation and global templates - Implement SSE streaming for AI code generation and iteration with colored progress stages - Add preview execution (fetch contacts + execute + results table) after generation and iteration - Auto-derive `attribute_id` from function name in SCREAMING_SNAKE_CASE - Add graceful error handling for SSE stream termination and API failures - Add shared function app selector (`select-function.ts`) used by fn subcommands - Update all fn subcommands to use shared app selector - Set `source: 'cli'` on all dp-functions API calls - Wrap SSE reader.read() errors into ApiError for clean failure messages - Add comprehensive tests for init command and SSE stream module Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use draft_id instead of code for post-iterate execute preview The backend updates the draft in the database during iterate (PATCH /generate/stream) in all auth modes, so referencing the draft by ID is consistent with the initial preview and avoids sending the full code payload to the execute endpoint. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: show friendly message when Brevo Functions is not enabled (403) The dp-functions backend returns 403 with code `feature_not_enabled` when the account lacks the dp-functions entitlement. Map this to a user-friendly message in both the REST client (ApiClient) and the SSE stream handler so all fn subcommands display: "Brevo Functions is not enabled for this account." Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: address SonarCloud issues — reduce complexity, fix nested ternaries - sse-stream.ts: extract performSSEFetch, handleSSEErrorResponse, readChunk, processSSELine, flushSSEState helpers to reduce cognitive complexity from 49 to ~11; replace nested ternary with extractErrorMessage function - init.ts: extract executePreview, saveGeneratedFunction, mergeGenerateResult, updateSpinnerFromEvent, accumulateResult helpers to reduce cognitive complexity of processGenerateStream (38→~8) and aiGenerationFlow (40→~11); combine consecutive push calls into single push with multiple args - list.ts: extract inner template literals to variables to eliminate nesting - templates/index.ts: replace nested ternary with if/else in resolveTemplateFlags - create.ts: replace nested ternary with if/else in buildCreatePayload; extract cacheCreatedAppCredentials to reduce createCommand complexity Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: address remaining SonarCloud warnings - Parameterize similar tests in sse-stream.test.ts and definitions.test.ts (S5976) - Add formatCellValue helper for safe object stringification in init.ts (S6551) - Replace boolean statusBadge function with ACTIVE_BADGE/INACTIVE_BADGE constants in list.ts (S2301) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: fix last S6551 warning in formatCellValue (init.ts) Use explicit type checks (string, number, boolean) instead of String(value) on unknown, so SonarCloud can verify no object reaches the default toString path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: hide attribute_id from predefined template preview table Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove Attribute ID line from template preview output The Attribute ID was shown both as a header line and could appear in the results table. Remove the explicit header line so the preview only shows the description and the data table. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR #69 review comments - Use shared functionService from container instead of local instance (init.ts) - Wire STAGE_LABELS to lang file keys instead of hardcoded strings - Rethrow ApiError/CliError in generate and iterate catch blocks - Guard iterate request and preview against missing draftId - Match duplicate-name detection on HTTP 409 status code, not message copy - Union columns across all rows in printResultsTable - Document AbortSignal.timeout as a deadline in sse-stream.ts - Evaluate statusBadge at call time to respect TTY/NO_COLOR (list.ts) - Change --id [id] to --id <id> in all fn subcommands - Remove orphaned lang keys (stage labels, success messages, file-written) - Hoist distribution_type out of template branches (app-config.json.tmpl) - Remove unused src/__tests__/tsconfig.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve SonarCloud issues — cognitive complexity and boolean param - init.ts: extract `runIterateRound` and `tryPreview` from `aiGenerationFlow` to reduce cognitive complexity from 21 to within the 15 threshold (typescript:S3776). - list.ts: replace `statusBadge(isActive)` boolean-param function with two named functions `activeBadge()` / `inactiveBadge()` (typescript:S2301). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add `brevo fn deploy` command for deploying draft functions Add a standalone deploy command that lets users deploy draft Brevo Functions independently of the `brevo fn init` AI generation flow. - New `brevo function deploy` command with `--id` and `--json` flags - Interactive draft picker when `--id` is omitted - Preview with sample contacts before deployment - Name prompt with duplicate-name (409) retry loop - Stop flow with "Unable to deploy function" when preview returns errors - Extract shared preview-table utilities from init.ts into preview-table.ts - 11 test cases covering success, JSON, picker, errors, and edge cases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve SonarCloud issues in deploy.ts — cognitive complexity and boolean param - Extract `deployJsonMode`, `deployInteractive`, and `tryPreview` from the main handler to reduce cognitive complexity (typescript:S3776). - Replace `assertDraftSelectionAllowed(jsonMode?)` boolean-param function with `assertInteractiveTerminal()` that only checks TTY (typescript:S2301). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: replace backtracking regex with lastIndexOf in deriveNameFromDescription Sonar flagged `/\s+\S*$/` as super-linear due to backtracking. Use `lastIndexOf(' ', 50)` instead — same word-boundary cut, O(n). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: link deployed function to app after creation Add app-linking step to `function deploy` and `function init` flows. After a function is created, it is linked to the selected app via POST /v3/app-store/app-functions. Adds --app-id flag to deploy for non-interactive use, and an interactive app picker when omitted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR #85 review — shared module, JSON safety, tests - Extract selectFunctionApp/tryLinkFunctionToApp into select-app.ts, used by both deploy.ts and init.ts (review point 3) - Fix --json output corruption: link runs silent, spinner uses { silent: true }, no logInfo to stdout (point 1) - Add linked/app_id to JSON payload so callers can detect link status (point 2) - Move app picker after preview+confirm to avoid wasted round-trips on early failure (point 4) - Remove appId! non-null assertion — narrowed via control flow (point 5) - Log link error detail behind isDebug via logDebug (point 6) - Add 5 new tests: --app-id path, --json without --app-id, link failure in JSON mode, no-apps throw, link failure warning (point 7) - Fix template literal + concatenation mix in app name display (point 8) - Add changeset for new --app-id flag (point 8) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract shared deploy helpers to reduce code duplication Move isDuplicateNameError, executePreview, tryPreview, and the name-confirm-deploy loop into deploy-helpers.ts. Both deploy.ts and init.ts now delegate to these shared functions, parameterized by message constants. This addresses the SonarCloud duplication quality gate (11.2% → well below 3%). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove changeset file Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve SonarCloud duplication and nested template literal - Generalize executePreview to accept templateArgs (draft_id or template_id) so init.ts templateFlow uses the shared function instead of an inline duplicate. - Fix nested template literal in select-app.ts (S4624). - Extract test setup into setupHappyPath() helper to reduce duplicated mock boilerplate across 16 tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: remove preview gate from brevo function commands (GA) Move the `brevo function` surface out of the `__BREVO_PREVIEW__` build gate so it ships in every published build. This follows the same pattern as the UI-apps GA transition (BEX-290). Changes: - Flip FEATURE_STAGE['brevo-function-type'] from 'preview' to 'ga' - Move function group definition from preview-definitions.ts to definitions.ts (always-defined, no longer conditional) - Add function handler imports directly in definitions.ts - Remove __BREVO_PREVIEW__ guard from bin/index.ts, help.ts, create.ts - Move all FUNCTION_* strings from preview-messages.ts to en.ts - Add GA markers (listFunctionCommand, initFunctionCommand, deployFunctionCommand) to scripts/build.mjs - Update tests to reflect the GA state Verified: yarn test (1513 pass), yarn build (public), PREVIEW=1 yarn build (preview), yarn lint, yarn format:check — all clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add function suite to smoke test live surfaces Include `function` in the smoke test's default and pinned suites so the GA'd Brevo Function commands are exercised on every publish and pre-merge lane — matching the CLAUDE.md requirement that a GA'd feature joins the smoke test in the same PR. - scripts/smoke-test.ts: add `function` to DEFAULT_SUITES - smoke.yml: update workflow_call/dispatch defaults and options - smoke-post-merge.yml: widen the pinned suite to `private,ui,function` Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: expand function smoke suite with mutation steps, init flow, and dedicated account Wire BREVO_TEST_API_KEY_FUNCTION secret and split function suite into its own CI job so it runs against a dedicated dp-functions account. Add activate/deactivate cycle, not-found error probes, deploy+cleanup, and pty-driven function init (template path) with automatic cleanup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve SonarCloud duplication in function smoke not-found probes Extract shared assertNotFound() helper from the four identical not-found error probe functions, bringing new duplicated lines density well under the 3% quality gate threshold. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve SonarCloud duplication in function action commands Extract shared function state-change pattern (ID resolution, spinner, 404 handling, JSON/card output) into buildFunctionActionCommand helper. Activate and deactivate become thin config wrappers; delete delegates execution while keeping its confirmation prompt. Shared logic is tested once in function-action.test.ts, individual tests verify wiring only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: reduce SonarCloud duplication across function commands and smoke tests Extract resolveFunctionId and withNotFoundHandling helpers for get.ts, deduplicate smoke test cleanup with deleteAndAssert and toggle helpers, and remove unused import flagged by code-quality bot. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add app_type field to app-config.json template Add explicit `app_type` field ('oauth', 'ui', or 'function') to the scaffolded app-config.json so the app type is immediately visible without relying solely on the presence of discriminator blocks. Addresses PR #100 review comment by @piyush. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: address PR #100 review comments - Add `function` to app-type registry with positive `brevo_function` detection on both config and record paths - Add `function` row to capability matrix (no OAuth flow, no redirect URIs, no account-install) - Add `brevo_function` field to `OAuthApp` type and `AppRecordLike` - Add `brevo function deploy` to help section - Replace IIFE with `resolveFunctionId` in `executeFunctionAction` - Add pagination to `fetchFunctionList` for >50 functions - Wire `ensureFresh` into `SSEStreamDeps` for token refresh before SSE connections - Expose `runEnsureFresh()` on `ApiClient`, plumb through container - Fix comment in templates/index.ts (`brevo_function` is mutually exclusive with oauth, not orthogonal) - Add explanatory comments in init.ts, tsconfig.eslint.json, help.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add backward compatibility and smoke tests for app_type field - Config backward compat: legacy configs without app_type parse correctly (field is undefined, not an error) - Config round-trip: app_type survives writeProjectConfig, and legacy configs without it stay absent after write-back - Wire isolation: app_type never leaks into the upload payload (UploadAppPayload has no app_type field) - Template rendering: brevo_function branch renders app_type correctly, all three app types produce the right value - Capability matrix: function type has no OAuth/UI capabilities, only review-lifecycle on public distribution - Record resolution: brevo_function record resolves to function type; blockless records still resolve to UI (not function) - Recoverability: function apps are always recoverable Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: add changeset for function app-type registry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: rename app_type to appType in config for camelCase consistency Aligns with the existing camelCase convention used by other config fields (appId, appName, logoUri). The field is local metadata only and never sent to the server, so this is a config-shape change with no wire impact. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: update stale doc comments for appType and add app_type migration - Fix doc comment in config.ts that said "there is no separate appType key" - Fix comment in en.ts that said "there is no app-type field" - Fix AGENTS.md.tmpl that said "There is no appType key" - Add legacy app_type → appType migration in readProjectConfigAt - Add 3 migration tests (read migration, write-back cleanup, precedence) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: revert appType back to app_type in config Rename the informational app-type field from camelCase `appType` back to snake_case `app_type` to match the wire-mirrored key convention used by other platform-facing fields (`distribution_type`, `ui_app`, `brevo_function`). The migration logic now migrates the legacy `appType` key to `app_type` on read and drops it on write-back. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * BEX-470 refactor(config): normalize app-config.json keys to snake_case with legacy migration (#104) * refactor(config): normalize app-config.json keys to snake_case with legacy migration Rename the camelCase keys in app-config.json to snake_case, matching the wire contract: appId → app_id, appName → app_name, logoUri → logo_uri, appType → app_type, auth.redirectUris → auth.redirect_uris. The ProjectConfig interface now IS the file shape, so every read site was found by the compiler. Backward compatibility: readProjectConfigAt folds the legacy camelCase keys (and the older auth.redirectUrls) through one alias table, drops them from the returned object, and warns once on stderr when a file carries both spellings with different values. Migration on write: create/upload/scaffold/start now emit snake_case only. upload and scaffold also rewrite an in-sync legacy file on their no-op paths via migrateProjectConfigKeys(), so one run migrates a project regardless of drift. Values are never changed. Also: template, scaffold docs, agent-context docs, README, smoke scripts (accept both spellings so published-build runs still pass), tests for camelCase-only / snake_case-only / mixed reads and the write-back migration, and a changeset. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(config): announce key migration on every rewrite and add agent migration note Print the snake_case migration notice whenever a legacy app-config.json is rewritten — after an upload push and after a consented scaffold refresh, not only on the no-op paths — and word it so users know to update their own scripts that read the file. Add an explicit migration rule to agent-context/SKILL.md and AGENTS.md so agents steer custom code to the new key names and never write the camelCase names. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(upload): assert the block error pre-empts the app_type label check Pin the ordering `assertAppTypeAgrees` documents: a config that is BOTH mislabelled and structurally broken must report the structural problem, so a partner is never sent off to fix a label only to hit the real refusal on the next upload. Asserted positively (the entry-named `.label:` error) plus an explicit check that the message does not mention `app_type`, rather than via `rejects.not.toThrow`, which can pass for the wrong reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: describe app_type as a validated label, not a discriminator `app upload` now reads `app_type` to check it agrees with the blocks, so three docs that said the CLI never reads it were wrong as written. - CLAUDE.md: read for validation, never for detection; the pre-flight ordering and why it is last; and the wire decision, which was nowhere stated — the field is file-only because `UploadAppPayload` and the create body are closed structs built key by key, so nothing strips it and bo-be needs no change. - The scaffolded AGENTS.md template said "the CLI never reads it". It now says what the field is for and that a hand-edited block needs the label updated. - agent-context/SKILL.md and AGENTS.md documented the key but not its meaning; both now carry the same paragraph, kept in sync per the repo rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(changeset): fold the snake_case entry into the branch's single changeset CLAUDE.md asks for one changeset file per branch, appended to rather than multiplied. Merges the separate snake_case file into the existing one and adds the app_type agreement check. Bump level stays `minor` — both entries already warranted it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> * fix: address PR #100 review comments for brevo function - Add Brevo Functions documentation to SKILL.md and AGENTS.md (was missing entirely — agents had no knowledge of the function commands) - Fix `brevo app upload` for Function apps: skip redirect_uris check, send `brevo_function: {}` on the wire, omit `auth` block, handle write-back and diff rendering correctly for the third app type - Paginate `fetchDraftFunctionList` (was capped at 50, causing silent truncation and false not-found errors in `brevo fn deploy --id`) - Fix `fetchFunctionList` pagination loop: use `has_more` + empty-page guard instead of `total` comparison to prevent infinite loops - Refactor `executeFunctionAction` to delegate to `withNotFoundHandling` instead of re-implementing the 404 catch block (Sonar duplication) - Move hardcoded strings (`Description:`, `Name:`, `Untitled Function`) to `src/lang/en.ts` - Remove unused `functionService` mock from function-action.test.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * BEX-471 feat(json): emit snake_case twins for every camelCase --json key (transition step) (#106) feat(json): emit snake_case twins for every camelCase --json key (transition step) Machine-readable output had two conventions: pass-through commands (app list, app submit, function *) emit the wire's snake_case, while CLI-built objects (create, upload, credentials, install, delete, whoami, the error envelope) emit camelCase. app-config.json settled on snake_case in BEX-470, and scripts should be able to read one spelling everywhere. Renaming outright would break every `jq .appId` in a pipeline with nothing the CLI could migrate, so this is the deprecation step: jsonOutput() now adds a snake_case twin after each camelCase key at the top level of every document (per element for arrays) and inside the error envelope. redirectUri / redirectUris alias to the wire name redirect_uris. Nested objects are left untouched — they are wire records or user data. Nothing is removed; camelCase goes away in the next major. Agent docs and the changeset carry the deprecation note; the seven tests that pinned exact camelCase shapes now pin both spellings. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Piyush Sarin <piyush.sarin@brevo.com> Co-authored-by: Piyush Sarin <84779634+piyushsarin-sib@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Step 1 of moving the CLI's machine-readable output to snake_case: every
--jsondocument now carries each camelCase key together with its snake_case twin. Nothing is removed, so every existingjq .appIdkeeps working. camelCase goes away in the next major (step 2, separate ticket).Jira: BEX-471 (follow-up to BEX-470). Base branch:
features_set-dp-function.Why
--jsonoutput mixed two conventions. Pass-through commands emit the wire's snake_case (app list→app_id,app submit→app_id/form_url,function *); CLI-built objects emit camelCase (create→appId/clientId/redirectUri,upload→upToDate,install/uninstall→accountId,whoami→organizationId, the error envelope →exitCode/statusCode). The smoke suite's ownpickIdhelper accepts three spellings because it cannot predict which one a command uses.app-config.jsonsettled on snake_case in #104; scripts should get one spelling too — but a hard rename is a breaking change the CLI cannot migrate for the user, hence the dual-key transition.What changed
src/lib/json-output.ts:withSnakeCaseAliases()runs insidejsonOutput(). For each camelCase key at the top level of the document (per element for an array document) and inside theerrorenvelope, a snake_case twin is inserted right after it. A twin never overwrites a key the object already has.redirectUri(app create) andredirectUris(app credentials) →redirect_uris.ui_appblocks, the upload diff'scurrent/next, Function records and contact data are either already wire-shaped or the user's own data, and rewriting keys inside them would change their meaning.jsonOutputcall sites get the behaviour from the one function.Examples from the built binary:
Docs
agent-context/SKILL.md(Hard rules) andagent-context/AGENTS.md(Conventions): new rule — both spellings are present, prefer snake_case, camelCase is deprecated and removed in the next major. JSON-errors sections and thescaffoldSkippedmention updated to show both keys..changeset/function-app-type-registry.md(minor).--jsonexamples, JSON errors section) need the same note; flagged in BEX-471 for product/docs.Tests
json-output.test.ts: newwithSnakeCaseAliasessuite — ordering, no-overwrite,redirect_urisoverride, multi-hump keys, shallow behaviour, array documents, error envelope, primitives, and thatjsonOutputwrites the aliased form.emitJsonErrorexpectation updated.delete,withdraw,install×2,uninstall×2) now pin both spellings.pickIdand thecreated.appName/created.redirectUriassertions keep passing; they switch to snake_case-only in step 2.Not in this PR (step 2, next major)
Removing the camelCase keys, tightening the smoke helper to snake_case only, and a breaking-change changelog entry.
Checks
yarn lint✅yarn test:ci✅ (70 suites, 1600 tests)yarn build✅yarn format:check✅🤖 Generated with Claude Code