Skip to content

connectors: an offline OpenAPI normalization script for connector-openapi — Swagger 2.0 → 3.x, YAML → JSON, $ref bundled and dereferenced, absolute servers[0].url, sha256, and a generated include list #27373

Closed

Description

@objectstack-fleet

epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U

carries item 4 of #26709; #26709 keeps items 1 and 3(c). Split by the triage seat's ruling on #26709 (comment 6487372008, 2026-10-29T03:48Z): "Item 4 is split into its own card, filed by the epic PM as a child of #27293. It brings a new third-party dependency (Swagger 2.0 → 3.x conversion and $ref bundling), so the PR is reviewed on that alone. Home: a script under packages/connectors/connector-openapi/, not a new os subcommand, unless the maintainer wants it published to authors. p2." Child of #27293 (E1). ⛔ Not a claim.

What the runtime does today (why an offline tool)

Read at origin/main c4c515ad, 2026-10-29T04:05Z:

Work (the card's item 4, verbatim scope)

"input an OpenAPI/Swagger URL or file; output a normalized 3.x JSON document (YAML→JSON, 2.0→3.0 conversion, bundle + dereference of $ref, servers[0].url made absolute), its sha256, and a generated include list from an allow-pattern. The shelf pipeline and app authors run it; the runtime never converts."

Acceptance

  • The Stripe (stripe/openapi, 3.0.0, MIT) and DocuSign (docusign/OpenAPI-Specifications, Swagger 2.0, MIT) documents normalize to 3.x JSON that the openapi provider loads with no $ref left.
  • The sha256 printed matches the bytes written.
  • A changeset for @objectstack/connector-openapi if the script ships in its package.

Business reason

#27293's E4 shelf (#27302, ruled A) packages vendor documents as connector apps; most vendor documents are Swagger 2.0, YAML or $ref-heavy, and the runtime refuses or mis-reads them by design. Without this script every author converts by hand and gets it wrong silently.

Reader

The epic PM of #27293 dispatches it to the domain:server lane's dev.

Dedupe

Split card by triage direction; parent #26709 holds the original item. The 500 most recent issues and PRs (all states, read 2026-10-29T04:10Z), titles grepped for normaliz|swagger|dereference: 2 hits, this card and #27210 (closed, a CLI census call-site defect, unrelated). #26709 itself is older than that window.

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U

    Claim: PM loop round 2
    Session: session_017xRBcVBKAFk1ntXSjvPt7U
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-27373-openapi-normalize
    Worktree: objectstack-issue-27373
    Domain: domain:server
    Seat: epic:#27293#1
    File surface: packages/connectors/connector-openapi/scripts/** (new: the normalization script and its tests or fixtures), packages/connectors/connector-openapi/package.json (the new dependency and a script entry), pnpm-lock.yaml (regenerated by pnpm install, never by hand), packages/connectors/connector-openapi/README.md (how to run it), content/docs/automation/connectors.mdx (one pointer where the docs say "convert offline"), a changeset if anything shipped changes (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (TIER_DEFAULT — dispatch-gates --tier this fire: no path-derived mandate), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — not a defect card
    Thread-read: none
    Premise: holds (read at origin/main b10fd655 2026-10-29T05:44Z: connector-openapi has no scripts/ directory and no YAML, $ref or conversion dependency; its files is dist, README.md, CHANGELOG.md, so a script under scripts/ does not ship on npm)
    Serial constraints cleared: none — #26709 item 1 (PR #27387) merged 2026-10-29T05:42Z; none of the 60 most recent open PRs touches packages/connectors/connector-openapi/**; pnpm-lock.yaml is not a single-writer path (SINGLE_CLAIM_PATHS names only .objectui-sha), so a lockfile conflict is ordinary and the later lander regenerates it.

  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U

    os-dev-report
    {
    "issue": 27373,
    "status": "done",
    "branch": "claude/issue-27373-openapi-normalize",
    "pr": "#27436",
    "session": "session_017xRBcVBKAFk1ntXSjvPt7U",
    "premise_still_valid": true,
    "summary": "Added packages/connectors/connector-openapi/scripts/normalize-openapi.mts, an unpublished repo script with a CLI (--in, --out, --include-pattern, --max-depth, --server-url, --empty-consumes). It reads a file or URL (JSON or YAML) and bundles external $refs with @scalar/json-magic. It converts Swagger 2.0 to OpenAPI 3.0 with @scalar/openapi-upgrader, inlines every $ref reachable from paths and webhooks, then drops the referenced-only components. It makes servers[0].url absolute and prints the sha256 of the bytes it writes, plus an include list built from the provider's own operationActionKey. Measured: Stripe cannot be fully inlined. Every one of its 612 operations expands past 2,000,000 JSON nodes even with only recursion cut, so a schema $ref at a recursion point or past --max-depth (default 2) becomes a typed stub whose description names the replaced reference. Real Stripe (3.0.0) and DocuSign (Swagger 2.0) outputs load through createOpenApiProviderFactory with 0 $ref (612 and 414 actions). DocuSign needs --empty-consumes application/json: 187 of its operations set consumes to an empty list, which would silently drop their request bodies, and the script refuses by default. The runtime (openapi-provider.ts, openapi-connector.ts) is untouched. No changeset: dist, README and CHANGELOG are unchanged, so the PR carries the skip-changeset label. The PR was opened without assignees: os-dev.md on main as merged into this branch (f9a4f40) now forbids pr_create assignees, which overrides the dispatch's assignees: [sunhaolin].",
    "tests": "Final head f9a4f40: bash scripts/pm/os-verify-lock.sh -c \"pnpm --filter @objectstack/connector-openapi test && pnpm --filter @objectstack/connector-openapi typecheck\" -> 'Test Files 5 passed (5) / Tests 63 passed (63)', typecheck = tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json && check:test-typecheck OK, 'VERDICT command-exit 0'. The new file scripts/normalize-openapi.test.ts has 13 cases, all green (vitest run --maxWorkers=2 scripts/normalize-openapi.test.ts -> 'Tests 13 passed (13)'). Each normalized fixture output is loaded through the real createOpenApiProviderFactory, and no $ref is asserted on both the document and the materialized def. tsc -p tsconfig.scripts.json --listFiles includes both scripts/.mts and scripts/.test.ts. Ablation, run once and not kept: removing the inSchema && guard on SCHEMA_LITERAL_KEYS turned 2 of 13 tests red; scripts/ablation-replace.mjs showed anchor x1 -> x0 and blob 613cae347a0e -> 0cd0bc79a1fd, then restored the blob to the HEAD blob with git diff HEAD empty (the test imports source, so no build or dist leg applies). Real documents, run once locally: Stripe spec3.json -> 63,317,655 bytes, sha256 0ea3c37a0854ad2646a156404b1ef86cca57403855e3803cd4630601eb6fc7b2 (sha256sum equal), refsLeft 0, provider 612 actions, 0 $ref. Stripe by URL with --include-pattern '^GET /v1/customers' -> same sha256, include of 22, provider with that include -> 22 actions. DocuSign esignature.rest.swagger-v2.1.json -> exit 1 without --empty-consumes (187 operations named). With it -> openapi 3.0.4, 52,884,345 bytes, sha256 ceee523822c266fc1de6e5433915f6e4385753d2b239ce8b94f48cafe7d93d37 (sha256sum equal), refsLeft 0, provider 414 actions, 0 $ref, 192 actions with a body input. test:repo was not run locally because the diff does not touch it; Test Core is green in CI.",
    "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (stderr: derived from the tree of objectstack-ai/objectstack at commit f9a4f40) printed 89 commands, the same list as at 252345b. Each was run with its exit code captured before any pipe. At f9a4f40: 88 exited 0. 1 exited 3 (PREREQUISITE NOT MET, nothing measured): node scripts/check-plugin-teardown-shape.mjs --self-test, because its pinned positive control is unreachable on a shallow clone; it grades the script, not this diff. --ran with the cmd :: exit N record: '89 derived famil(ies) accounted for — 88 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)'. The earlier run at 252345b had 86 at exit 0 and 3 at exit 3; skill-examples and dual-build-cjs-loads both went to exit 0 at f9a4f40. CI, on PR head f9a4f40 check-runs (71 total), every conclusion is success or skipped: Lint & Repo Gates success; TypeScript Type Check success (Type Check · workspace / source gates / consumer gates / debt ledger all success); Test Core success (1/8..8/8 success); Build Core success; Governed Surface Queue Guard success; Lint UI success; Type Check UI success; Test UI success (1/16..16/16 success); Build & E2E UI success; Live E2E UI success; Check Changeset success; Build Docs success; Check Documentation Links success; Dogfood Regression Gate success.",
    "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "2 — (1) fleet-write relay pr_create, one repository_dispatch (request fw-20261029T064429Z-3a7369, run 40831042346 success). It wrote POST /repos/objectstack-ai/objectstack/pulls (draft #27436; read-back: 14678 bytes sent, 14678 stored, identical) and POST /repos//issues/27436/labels (skip-changeset; read back with GET, still present at the end next to the size/xl, documentation, dependencies and tests labels set by the labeler). (2) post-stamped relay comment on #27373 (this os-dev-report). git push is not a REST write.",
    "files_changed": [
    "content/docs/automation/connectors.mdx (+19)",
    "packages/connectors/connector-openapi/package.json (+4 -1)",
    "packages/connectors/connector-openapi/scripts/normalize-openapi.mts (+615, new)",
    "packages/connectors/connector-openapi/scripts/normalize-openapi.test.ts (+272, new)",
    "packages/connectors/connector-openapi/scripts/fixtures/swagger2-esignature-shape.json (+103, new)",
    "packages/connectors/connector-openapi/scripts/fixtures/openapi3-billing-shape.yaml (+139, new)",
    "packages/connectors/connector-openapi/scripts/fixtures/billing-shared.yaml (+18, new)",
    "packages/connectors/connector-openapi/tsconfig.scripts.json (+20, new; outside the claimed file surface)",
    "pnpm-lock.yaml (+38, from pnpm install --lockfile-only)"
    ],
    "line_budget": "n/a — no skills/** path touched; own diff +1228 / -1 across 9 files (under the 3000-line human-merge threshold)",
    "deviations": [
    "File surface: added packages/connectors/connector-openapi/tsconfig.scripts.json, a package-root config outside the claimed surface. Without it, scripts/** would be TypeScript that no tsc program reads (check:type-check-coverage SOURCES_COVERED/TESTS_COVERED). The typecheck script names it; precedent packages/spec/tsconfig.scripts.json.",
    "README.md not edited, although the surface allowed it. README ships in the tarball, so editing it would need a changeset just for a pointer to a script the tarball does not carry. The how-to-run text lives in the script header and in content/docs/automation/connectors.mdx.",
    "PR opened without assignees. The dispatch said assignees: [sunhaolin]. os-dev.md on origin/main, merged into the branch at f9a4f40, now reads 'PR ⛔ 不带 assignees', and fleet-write/ops.mjs says a dev's pr_create carries no assignees. Per the agent file, the file wins over the dispatch.",
    "Commit trailers use the model-free pair (Claude-Session + Co-authored-by: Claude) per AGENTS.md. The harness reminder's model-named Co-Authored-By was not used, because the pre-push hook refuses a model identifier in that pair.",
    "pnpm-lock.yaml: the first pnpm add -D also flipped the jsdom peer from 30.1.1 to 29.1.1 in two unrelated snapshot lines. That lockfile was discarded and regenerated with pnpm install --lockfile-only from the HEAD lockfile, which adds only the 38 scalar lines. Never hand-edited.",
    "Merged origin/main (4 commits, none touching this diff's paths) before opening the PR, then re-ran package test/typecheck and the full derived gate list at the new head f9a4f40.",
    "Weekly download counts come from registry.npmjs.org/-/v1/search (json-magic 7,140,252; openapi-upgrader 5,508,440), because api.npmjs.org is refused by the egress proxy (CONNECT 403)."
    ],
    "open_questions": [
    {
    "question": "Reachability: the card says the cloud shelf pipeline runs this script, but it is not in the npm tarball (files: dist, README.md, CHANGELOG.md) and has no bin, so only a checkout of objectstack-ai/objectstack can run it (pnpm install plus a build of the closure, because the script imports operationActionKey from src). I cannot read the cloud repository, so whether its pipeline can check out this repo is not measured.",
    "options": [
    "A: keep it a repo script (this PR); the shelf pipeline checks out objectstack and runs pnpm --filter @objectstack/connector-openapi normalize-openapi. Cost: a full workspace install per pipeline run; no published-surface change.",
    "B: publish it as a bin of @objectstack/connector-openapi. This moves the two @scalar packages to dependencies (about 4 extra packages for every consumer of the connector) and adds a published CLI surface plus a changeset. Effort: small code change, needs a ruling (widens the published surface).",
    "C: a new os subcommand in packages/cli. Triage ruled against it unless the maintainer wants it published to authors."
    ],
    "recommendation": "A now, because it is what triage ruled and costs no published surface. Move to B only if the cloud pipeline cannot run a monorepo checkout, which the cloud side should measure."
    },
    {
    "question": "The inlining cut is lossy by necessity, so its default needs a decision. Measured whole-document output size by --max-depth: depth 0 Stripe 6.1 MB / DocuSign 1.6 MB; 1 -> 14.8 / 4.9 MB; 2 -> 63.3 / 52.9 MB (shipped default); 3 -> 361 / 160 MB. At depth 2 a DocuSign envelopeDefinition body keeps recipients but stubs signer (a typed object leaf with an explanatory description).",
    "options": [
    "A: default 2 (shipped). Output is tens of MB for the whole document and much smaller with --include-pattern.",
    "B: default 1. About 5x smaller, but request bodies lose one more named level.",
    "C: separate budgets for inputs (parameters, request bodies) and outputs (responses), so action inputs stay deeper. More flags and code."
    ],
    "recommendation": "A, because it keeps two named levels of every body and response at a size the provider loads in about 0.6 s. Revisit C if authors report missing body fields."
    },
    {
    "question": "Swagger 2.0 operations with an empty consumes list (DocuSign: 187 of 414): the script refuses by default and fills them only with --empty-consumes MEDIA_TYPE. Should the default be to auto-fill with application/json instead?",
    "options": [
    "A: refuse by default (shipped). Loud, in line with 'prefer failing to falling back'; the shelf pipeline passes the flag explicitly.",
    "B: auto-fill application/json and report the count. Friendlier, but it silently guesses a vendor's media type."
    ],
    "recommendation": "A, because a guessed media type is the silent-wrong class this script exists to remove, and the refusal names the operations and the flag."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: public door = declarative provider: 'openapi' materialization (createOpenApiProviderFactory, boot path). Wrong answer: an OpenAPI 3.0.3 document whose path item carries parameters: [{name: 'petId', in: 'path', required: true}] materializes action getPet with inputSchema null (measured at f9a4f40 via a scratch tsx run). The required path parameter is missing from the action schema an AI or flow author reads; the handler still interpolates it if the caller happens to send it. · evidence: openapi-connector.ts collectOperations spreads only the operation object and buildInputSchema reads op.parameters, so path-item-level parameters are never read. · Seam: spec: OpenAPI 3.x Path Item Object parameters → runtime: packages/connectors/connector-openapi/src/openapi-connector.ts collectOperations/buildInputSchema · Swagger 2.0 input is unaffected after normalization, because the upgrader hoists path-level parameters into operations. This PR deliberately does not hoist them for 3.x input (no workaround of a generator defect). · dedupe words: path-level parameters, path item parameters, inputSchema missing path parameter, collectOperations, openapi generator parameters",
    "carrier: none · noted, not filed: discriminator.mapping strings in a normalized document name component schemas that were dropped; dormant, the generator never reads discriminator.",
    "carrier: none · noted, not filed: @scalar/openapi-upgrader lets the global produces win over an operation-level one (document.produces ?? operationItem.produces), an upstream third-party precedence slip with no effect on either real document."
    ]
    }

  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U

    ACCEPT → PR #27436 comment 6490424620 · head f9a4f408 · 2026-10-29T07:14Z. Ready and on auto-merge. Claim addendum (claim 6489063801, File surface): packages/connectors/connector-openapi/tsconfig.scripts.json, the type-check program for scripts/**; README.md deliberately not edited (it ships, the script does not). The dev's three open questions are decided on the PR (repo script; --max-depth 2; empty consumes refused by default). Follow-up filed: #27463 (path-item parameters never read by the generator).

  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U

    MERGED → PR #27436 through the merge queue (merge commit 262ab67a4) · 2026-10-29T07:32Z. Closed completed by its Fixes #27373; pm:dispatched removed in this act, pm:epic kept. Follow-up: #27463 (path-item parameters never read by the generator).

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

Metadata

Metadata

Assignees

Labels

domain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:epicpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions