Skip to content

feat: identity-keyed codegen pipeline (manifest-pinned names, multi-doc merge, classified releases) - #32

Merged
ProductOfAmerica merged 19 commits into
mainfrom
fix/spec-identity-pipeline
Jul 2, 2026
Merged

feat: identity-keyed codegen pipeline (manifest-pinned names, multi-doc merge, classified releases)#32
ProductOfAmerica merged 19 commits into
mainfrom
fix/spec-identity-pipeline

Conversation

@ProductOfAmerica

Copy link
Copy Markdown
Owner

Why

The daily Sync John Deere API workflow was red from 2026-06-24 through 2026-07-01. Root cause: generated method names derived from spec document order. When JD re-published the field-operations spec with reordered paths and a second document, FieldOperationsApi.get silently rebound to the shapefile endpoint and the build broke (the near-miss was worse: for callers not passing embed, the rebinding compiles and calls the wrong endpoint). Separately, the fetch kept only portal array element [0], silently dropping 15 of 43 spec documents.

What

  • Committed operation-identity manifest (scripts/api-surface.yaml): every public method name pinned to (HTTP method, normalized path). Upstream reorders are provably inert (the branch canonicalizes every committed spec, changing every path's position, with a byte-identical manifest and generated surface). Removals fail the sync loudly with per-operation remediation; new operations get deterministic proposed names appended automatically.
  • Multi-document merge: fetch validates every portal document and structurally merges them (repo-owned primary table, deep-equal dedupe, conflict renames with subtree ref rewriting to a fixpoint, platform-family servers reconciliation).
  • Canonical spec key order at the fetch boundary, so upstream churn diffs clean.
  • Loud failure end-to-end: all pipeline entrypoints exit nonzero on any failure; a parse failure can no longer silently drop an API class.
  • Classified release gate in sync-api.yml: benign (no release) / additive (auto minor, ops listed in the changelog) / breaking (job fails with manifest runbook guidance). Portal-derived strings are validated and env-routed, never shell-interpolated.
  • 56 new public operations from the previously-dropped documents (table below), with all 146 pre-existing bindings preserved byte-for-byte.
  • Equipment type restoration: JD's 2026-07 doc edit dropped two response item refs (schemas still defined); a guarded, self-neutralizing fix-specs transform restores EquipmentApi.get/getEquipment return types to the previously shipped surface.

Verification

437 tests (including property tests for order-invariance, merge permutation laws, and the fixpoint rename cascade), lint, both typechecks, build, fuzz: all green. Byte-identity gates at every regeneration; idempotence proven by matching git tree hashes; the reorder-immunity claim executed against the full real spec corpus; 146/146 binding preservation verified by before/after extraction; final whole-branch review passed with its three findings fixed and re-reviewed.

Post-merge follow-ups

  1. Release: npm version minor && git push --follow-tags (CHANGELOG 2.4.0 entry is in this PR; do not patch-bump).
  2. GitNexus: the index was broken all branch (LadybugDB v41 vs MCP server v40; the npx repair hits npm 11's ECOMPROMISED bug). Upgrade gitnexus globally, reindex from the main checkout, run detect_changes({scope: "compare", base_ref: "main"}) as the deferred repo-mandated check.
  3. Watch the first scheduled sync: expected classification benign, no release (the end-to-end proof of the new gate).
  4. Deferred minor cleanups (all triaged FOLLOW-UP in the final review): extractOps dead code, shared isRecord helper, servers warning wording edge cases, vnd content-type test branch.

New operations (the names below become permanent public API on merge)

spec op proposed name source document
field-operations-api GET /fieldOperations/{operationId}/measurementTypes listMeasurementTypes measurement-type
field-operations-api GET /fieldOperations/{operationId}/measurementTypes/{measurementType} getMeasurementTypes measurement-type
files GET /fileTransfers listFileTransfers file-transfers
files GET /fileTransfers/{id} getFileTransfers file-transfers
files GET /organizations/{orgId}/fileTransfers listOrganizationsFileTransfers file-transfers
files POST /organizations/{orgId}/fileTransfers createFileTransfers file-transfers
flags DELETE /organizations/{orgId}/flagCategories/{categoryId} deleteFlagCategories flag-categories
flags GET /organizations/{orgId}/flagCategories listFlagCategories flag-categories
flags GET /organizations/{orgId}/flagCategories/{categoryId} getFlagCategories flag-categories
flags GET /organizations/{orgId}/flagCategories/{categoryId}/flagCategoryPreferences listFlagCategoryPreferences flag-categories-preferences
flags GET /organizations/{orgId}/flagCategoryPreferences/{flagCategoryPreferencesId} getFlagCategoryPreferences flag-categories-preferences
flags POST /organizations/{orgId}/flagCategories createFlagCategories flag-categories
flags PUT /organizations/{orgId}/flagCategories/{categoryId} updateFlagCategories flag-categories
flags PUT /organizations/{orgId}/flagCategoryPreferences/{flagCategoryPreferencesId} updateFlagCategoryPreferences flag-categories-preferences
machine-locations GET /machines/{principalId}/breadcrumbs listBreadcrumbs breadcrumbs
map-layers DELETE /fileResources/{id} deleteFileResources file-resources
map-layers DELETE /mapLayers/{id} deleteMapLayers map-layers
map-layers GET /fileResources/{id} getFileResources file-resources
map-layers GET /mapLayerSummaries/{id}/mapLayers listMapLayers map-layers
map-layers GET /mapLayers/{id} getMapLayers map-layers
map-layers GET /mapLayers/{mapLayerId} getMapLayersByMapLayerId map-layers
map-layers GET /mapLayers/{id}/fileResources listFileResources file-resources
map-layers POST /mapLayerSummaries/{id}/mapLayers createMapLayers map-layers
map-layers POST /mapLayers/{id}/fileResources createFileResources file-resources
map-layers PUT /fileResources/{id} updateFileResources file-resources
products GET /activeIngredients listActiveIngredients active-ingredients
products GET /chemicals listChemicals chemicals
products GET /chemicals/{erid} getChemicals chemicals
products GET /chemicals/{erid}/documents listChemicalsDocuments chemicals
products GET /documents/{erid} getDocuments documents
products GET /fertilizers listFertilizers fertilizers
products GET /fertilizers/{erid} getFertilizers fertilizers
products GET /fertilizers/{erid}/documents listFertilizersDocuments fertilizers
products GET /organizations/{organizationId}/chemicals listOrganizationsChemicals chemicals
products GET /organizations/{organizationId}/chemicals/{erid} getOrganizationsChemicals chemicals
products GET /organizations/{organizationId}/dryBlends listDryBlends dry-blends
products GET /organizations/{organizationId}/dryBlends/{erid} getDryBlends dry-blends
products GET /organizations/{organizationId}/fertilizers listOrganizationsFertilizers fertilizers
products GET /organizations/{organizationId}/fertilizers/{erid} getOrganizationsFertilizers fertilizers
products GET /organizations/{organizationId}/productCompanies listProductCompanies companies
products GET /organizations/{organizationId}/tankMixes listTankMixes tank-mix
products GET /organizations/{organizationId}/tankMixes/{id} getTankMixes tank-mix
products PATCH /chemicals/{erid}/setOverridesForOrg/{organizationId} patchChemicalsSetOverridesForOrg chemicals
products PATCH /fertilizers/{erid}/setOverridesForOrg/{organizationId} patchFertilizersSetOverridesForOrg fertilizers
products POST /chemicals/{erid}/associateToOrg/{organizationId} createChemicalsAssociateToOrg chemicals
products POST /fertilizers/{erid}/associateToOrg/{organizationId} createFertilizersAssociateToOrg fertilizers
products POST /organizations/{organizationId}/chemicals createChemicals chemicals
products POST /organizations/{organizationId}/dryBlends createDryBlends dry-blends
products POST /organizations/{organizationId}/fertilizers createFertilizers fertilizers
products POST /organizations/{organizationId}/tankMixes createTankMixes tank-mix
products PUT /organizations/{organizationId}/chemicals/{erid} updateChemicals chemicals
products PUT /organizations/{organizationId}/dryBlends/{erid} updateDryBlends dry-blends
products PUT /organizations/{organizationId}/fertilizers/{erid} updateFertilizers fertilizers
products PUT /organizations/{organizationId}/tankMixes/{id} updateTankMixes tank-mix
webhook GET /eventSubscriptionDelivery listEventSubscriptionDelivery event-subscription-delivery
webhook PATCH /eventSubscriptionDelivery patchEventSubscriptionDelivery event-subscription-delivery

Identity-keyed codegen: committed api-surface manifest, multi-doc spec
merge, canonicalization, and a classified release gate. Eliminates the
class of breakage where JD spec reorders/splits rebind generated method
names or silently drop documents.
Entrypoints swallowed thrown errors (main().catch(console.error) exits 0)
and fix-specs swallowed per-file failures, so CI could ship stale specs on
a green run. Generators embedded wall-clock timestamps, making output
nondeterministic. Fail loudly, drop the timestamps, sort the types-index
read order, and ignore upcoming scratch files.
Relocate inferMethodName, the order-sensitive collision loop, and the
string helpers out of the generate-sdk entrypoint. generateMethod now
consumes a precomputed name. Regenerated output is byte-identical; the
extracted module exists only to seed the upcoming api-surface manifest
and is scheduled for deletion.
Pure library for the committed operation-identity registry: loader with
loud validation, deterministic serializer with an embedded runbook,
shared op extraction, order-independent name resolution, and a
deterministic proposer for new operations (no positional counters).
normalizePathPattern moves here so manifest identity and HATEOAS
matching share one implementation.
… name

crop-types declares GET /cropTypes/{name} and GET /cropTypes/{id} as
distinct operations, which collide under normalize-all-params identity.
Entries sharing a normalized key are now legal when their raw paths
differ; matching inside such ambiguous groups is by exact path, so a
param rename there surfaces as a breaking diagnostic instead of a
silent mismatch. Single-sibling groups keep absorbing param renames.
Replay the legacy order-sensitive naming over the committed fixed specs
(still pre-incident path order) to pin every current public method name
to its operation identity. A permanent test loads the manifest and locks
the known-critical bindings so neither hand edits nor auto-appends can
silently move the published surface.
The generator now resolves every public method name from the committed
operation-identity registry instead of inferring it from spec document
order. Unknown upstream operations get deterministic proposed names and
are appended to the manifest; a manifest operation missing upstream
fails the run with a per-entry diagnostic before anything is written.
Every run emits sync-report.json (gitignored) for the workflow gate.
Regeneration from committed specs is byte-identical, so the published
surface is unchanged; the legacy order-sensitive naming, its tests, and
the one-time seed script are deleted.
Sorts the paths map and every components category map so semantically
null upstream reorders produce byte-identical spec files at the fetch
boundary. Stringification matches fix-specs' emission options plus
aliasDuplicateObjects: false, so deduplicated merged components can
never surface as YAML anchors.
The portal returns multiple documents for 7 of 28 slugs; the fetch
keeps element zero and silently drops the rest. Validate every element
and structurally merge a slug's documents: repo-owned primary table
(never portal array order), method-level path union with deep-equal
dedupe, component conflict renames with subtree ref rewriting to a
fixpoint, server agreement checks, and source-document stamping.
check-api-health now checks every returned document.
fetch-specs validates all returned documents, structurally merges
multi-document slugs through the repo-owned primary table, and writes
canonicalized YAML so upstream reorders produce byte-identical raw
files. The single-document validator is retired; summary.json records
the merged document inventory per slug.
Rewrite specs/raw and everything downstream through the canonical
paths/components ordering that fetch-specs now emits, so future live
fetches diff cleanly and upstream reorders vanish. Every path in every
spec changes position while scripts/api-surface.yaml is byte-identical,
which is this branch's reorder-immunity proof executed against the
real spec corpus.
The sync now runs the full pipeline (redact-specs and
generate-api-servers were missing), reads sync-report.json after
generation, and acts on the classification: additive changes release a
minor version with the new operations listed in the changelog, benign
churn keeps patch semantics, and breaking runs fail at the Generate SDK
step with manifest remediation guidance in the job summary.
… docs

Live portal data showed JD's secondary documents carry servers blocks
that differ only as environment instances or documentation defects of
the one platform family (api vs partnerapi hosts, missing /platform
segments, placeholder hosts, omitted variables). Declaring documents
that all belong to the platform family now resolve to the primary's
block, which fix-specs normalizes to the templated form downstream,
matching what the SDK already ships for these specs. Placeholder-only
blocks are treated as non-declaring with a logged warning. Genuinely
different server families still refuse to merge.
First live fetch through the multi-document pipeline. The 7 multi-doc
slugs now merge every portal document: 15 secondary documents the old
fetch silently dropped are merged in, adding 56 operations JD published
under those documents (field-operations measurementTypes; the products
input families chemicals, fertilizers, dry-blends, tank-mixes,
active-ingredients, companies, documents; flags categories and category
preferences; map-layers and file-resources; file transfers; webhook
delivery; machine-location breadcrumbs). New operations enter
scripts/api-surface.yaml with deterministic proposed names; the run
classifies additive with zero missing operations.

Every previously published method keeps its exact name and binding,
verified by a before/after extraction of all 146 generated
(class, method, operation) triples: 146 preserved, 0 changed, 56 added.
src/api-servers.generated.ts and src/deere.ts are unchanged, so the
platform-family servers reconciliation routes nothing differently.

equipment.yaml carries an unrelated upstream JD edit (two response
schemas changed from a typed array-item ref to a bare array) that
weakens two EquipmentApi return types to unknown; no binding changes and
the full suite (lint, typecheck, build, 432 tests, 12 fuzz) passes.
JD's 2026-07 equipment doc edit dropped the values.items refs from two
responses while leaving the target schemas defined, collapsing
EquipmentApi.get and getEquipment returns to unknown. A guarded
fix-specs transform restores the refs while both conditions hold and
no-ops once JD repairs the doc or removes the schemas. Two unreleased
products action names gain their family prefix for symmetry with the
fertilizer twins via the manifest's hand-edit point.
…final review

Validate the sync classification against its closed set, neutralize
newlines in portal-derived strings emitted to workflow outputs, and
route classification through step env instead of shell interpolation.
Fail generate-sdk when any spec fails to parse (a dropped spec was
invisible to the missing-operation detector) and generate-types when
any per-file generation fails. Append the execution amendments to the
committed design plan so the durable record matches the implementation.
Main's legacy sync went green on 2026-07-02 (upstream reverted the
field-operations reorder) and released v2.3.1 carrying the equipment
type regression this branch counters. Resolution: canonicalized branch
artifacts win for all spec and generated files (main's fresh equipment
raw spec is canonically byte-identical to ours, verified), the 2.3.1
changelog entry slots beneath 2.4.0, and the full pipeline regenerated
over the merged tree, restoring the typed equipment returns the
textual auto-merge had lost. All gates green on the result.
@ProductOfAmerica
ProductOfAmerica merged commit cb76d2b into main Jul 2, 2026
9 checks passed
@ProductOfAmerica
ProductOfAmerica deleted the fix/spec-identity-pipeline branch July 2, 2026 15:43
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.

1 participant