ci+chore: version-alignment gate + resync package-lock to 5.6.0#182
Merged
Conversation
TS SDK's release workflow bumps package.json and regenerates src/version.ts at publish time but never commits the bump back. The repo stays in sync for those two files on merge (because the feature PR that ships a version bump updates them), but the package-lock.json was left at 5.5.0 from the prior release even as package.json moved to 5.6.0 — a real drift this PR caught and fixed before anyone noticed. Two things in this PR: 1. validate-version-alignment CI gate — a new workflow + script, patterned on the axonflow-enterprise script of the same name and the newly-landed equivalents in axonflow-sdk-go (PR #130) and axonflow-sdk-python (PR #153). Fails any PR or push to main that leaves package.json, src/version.ts, or package- lock.json out of sync with the most recent ## [X.Y.Z] section in CHANGELOG.md. 2. package-lock.json resync — regenerated via `npm install --package-lock-only` so it matches package.json at 5.6.0. This was real drift (lock said 5.5.0, everything else 5.6.0) that the new gate surfaced on its first local run. The invariant on main going forward: package.json::version == src/version.ts::VERSION == package-lock.json::version == first ## [X.Y.Z] section in CHANGELOG.md Release-prep PRs going forward must rename [Unreleased] → [X.Y.Z] AND bump the three manifest files (via `npm version X.Y.Z --no-git-tag-version` + `npm run stamp-version`) in the same commit, so the gate sees them as consistent throughout. Verified locally: - Pre-fix state (lock 5.5.0, everyone else 5.6.0) → gate fires with a clean 'expected 5.6.0' diff on package-lock.json and the correct remediation command (`npm version`) - Post-fix state (all at 5.6.0) → gate passes
Under 'set -euo pipefail', the LATEST_VERSION extraction would abort
the whole script before reaching the friendly '-z' error check when
CHANGELOG has no released section (e.g. fresh repo, or someone
deleted the last section). The grep's non-zero exit failed the
command substitution, which set -e treated as a script-wide abort.
Wrapped the grep in { ... || true; } so the -z branch fires with
the intended error message. Same fix now live in the Go arm
(PR #130) and Python arm (PR #153).
Verified by running the exact pipeline pattern against a no-match
input outside the script: reaches LATEST=[] and the -z branch,
no silent abort.
…es[""].version Review finding: the script comment claimed to check both the top-level package-lock.json.version AND the root-package entry packages[""].version, but the implementation only read the top-level one. That left a real silent-drift path — a PR could keep top-level aligned while the root-package entry stayed stale and the gate would still pass. npm keeps both fields synchronized through normal flows (npm install, npm version), but manual edits, partial merges, and tool quirks can desync them. Either field being stale breaks the npm publish / npm ci invariants. Fix: validate both fields independently. Error message points at which specific field drifted so the remediation is unambiguous. Verified locally: - Baseline (all at 5.6.0) → PASS - packages[""].version manually set to 5.5.0 while top-level stays 5.6.0 → gate fires cleanly on 'packages[""].version is "5.5.0", expected "5.6.0"' with the correct field named - Post-revert (npm install --package-lock-only) → PASS again
5 tasks
saurabhjain1592
added a commit
that referenced
this pull request
Apr 25, 2026
CI-internal hardenings; no user-visible behaviour change. - refresh.js: `--sha` with no value used to be silently consumed as the positional `specsDir`, masking the missing argument and routing the script through the gitHeadSHA fallback. Now exits 2 with a clear message. - validate.js: an empty `registered_types` paired with non-empty cross_spec / intra_file / per_type_drift entries used to silently disable Gate 4 (rename-escape). The validator now treats this combination as a baseline corruption and fails the run. A genuinely fresh baseline (every section empty) still skips the gate as before, since the first-pin run has nothing to guard. - lib.js writeBaseline: a crash between writeFileSync(tmp, ...) and renameSync(tmp, BASELINE_PATH) used to leave a `.tmp.<pid>` sidecar, which a future writer with the recycled PID would collide on. Now wraps in try/catch + unlinkSync on any throw. - lib.js extractInterfaceProps: an `interface X extends Y` would silently under-report fields because we walk only iface.members. No such interface ships in the SDK today; the gate now warns loudly the first time one appears so coverage gets resolved before drift can hide. Full heritage walking is the proper fix if this becomes a routine pattern. Verifications: - refresh.js with `--sha` as last token exits 2; with `--sha foo` works. - validate.js with corrupted baseline (empty registered_types but populated drift/cross_spec) exits 1 with explanatory message. - writeBaseline temp-file is cleaned up on synthetic write error. - Real baseline regenerates byte-identical to origin/main.
saurabhjain1592
added a commit
that referenced
this pull request
Apr 25, 2026
* fix(wire-shape): close 4 review gaps from #182 + #184 follow-up CI-internal hardenings; no user-visible behaviour change. - refresh.js: `--sha` with no value used to be silently consumed as the positional `specsDir`, masking the missing argument and routing the script through the gitHeadSHA fallback. Now exits 2 with a clear message. - validate.js: an empty `registered_types` paired with non-empty cross_spec / intra_file / per_type_drift entries used to silently disable Gate 4 (rename-escape). The validator now treats this combination as a baseline corruption and fails the run. A genuinely fresh baseline (every section empty) still skips the gate as before, since the first-pin run has nothing to guard. - lib.js writeBaseline: a crash between writeFileSync(tmp, ...) and renameSync(tmp, BASELINE_PATH) used to leave a `.tmp.<pid>` sidecar, which a future writer with the recycled PID would collide on. Now wraps in try/catch + unlinkSync on any throw. - lib.js extractInterfaceProps: an `interface X extends Y` would silently under-report fields because we walk only iface.members. No such interface ships in the SDK today; the gate now warns loudly the first time one appears so coverage gets resolved before drift can hide. Full heritage walking is the proper fix if this becomes a routine pattern. Verifications: - refresh.js with `--sha` as last token exits 2; with `--sha foo` works. - validate.js with corrupted baseline (empty registered_types but populated drift/cross_spec) exits 1 with explanatory message. - writeBaseline temp-file is cleaned up on synthetic write error. - Real baseline regenerates byte-identical to origin/main. * fix(wire-shape): close stale cross-spec baseline shielding gap Gate 1 (cross-spec divergence) only iterated currently observed schemas. A baselined divergence that the platform has since reconciled silently lingered in the baseline forever; the same old incompatible shape could be reintroduced and pass the gate because its fingerprint matched a stale entry that should have been deleted. Adds the reverse pass that Gate 2 already does for intra-file duplicates: any baselined cross-spec name that is no longer observed in the current specs fails the run with a pointer at the specific baseline key to delete. Mirrors the same fix landing on the Java arm in PR #137. Verified locally: - Positive run on clean baseline still exits 0. - Adding a phantom 'PhantomDivergence' entry to baseline cross_spec_duplicates causes the validator to exit 1 with a clear remove-from-baseline message naming the stale key. * chore: remove stray session-export artifact from previous commit The accidental git add -A in the prior commit swept up a session-export .txt file that lives in this worktree. Removing. * fix(wire-shape): canonicalize TS interface fields to wire snake_case The TS validator was extracting in-memory property names (camelCase by TS idiom) and diffing them directly against OpenAPI snake_case wire names, generating ~22 false-positive drift entries (Category A in the audit) where the SDK's hand-rolled transformer in client.ts already correctly bridged the two names. The other three SDK validators don't have this issue: pydantic alias for Python, `json:"…"` struct tag for Go, `@JsonProperty(…)` for Java — each extracts the WIRE name natively. TS has no first-class equivalent. This change adds equivalent wire-name extraction for TS: - toSnakeCase() converts camelCase to snake_case with three boundaries (acronym→word, lowercase→uppercase, lowercase→digit). The digit boundary fixes ModelPricing.inputPer1k → input_per_1k, which the earlier categorization regex got wrong. - loadTransformerEvidence() reads src/client.ts once and collects every snake_case-shaped identifier. This is the bridge audit: a TS field is treated as 'bridged to its snake_case wire form' only when the snake form actually appears somewhere in client.ts. - canonicalizeWireNames() applies the conversion per-field. If the snake form is found in client.ts, the field is normalized; if not, the camelCase form is kept and the type is recorded as having unbridged fields. - The safety net: a type's unbridged fields are warned about ONLY when the same type has at least one OTHER field that IS bridged. If no field in the type is bridged, the type is almost certainly SDK-internal (configs, options, adapter metadata, etc.) and the whole type isn't wire-bound; warning would be noise. Effect: 22 Category A drift entries disappear from the baseline (AuditToolCallRequest, BudgetCheckRequest, FEATAssessment, KillSwitch, ModelPricing, PlanVersionEntry, PolicyMatch, etc.). 63 → 41 drift entries. The 19 unbridged-field warnings the validator now emits all line up with audit-flagged broken/orphan reads in client.ts (PolicyEvaluationResult.databaseAccessed, StaticPolicy.hasOverride, StepGateResponse.policiesMatched, etc.) — the validator is now correctly distinguishing 'bridged drift' from 'genuinely missing transformer'. No SDK property names changed. Wire behavior is unchanged. This is purely a validator measurement fix; the SDK's existing transformer architecture is preserved as-is. Per platform-side spec corrections needed (filed separately): - AISystemRegistry.materiality_classification (#1708) - DynamicPolicyInfo schema completely wrong (#1709) * feat(types): wire-shape alignment sweep (Cat B + RENAME_SAFE + UNRECOVERABLE) Audit-driven cleanup against the wire-shape contract gate. All changes are additive (new fields are optional). Where a TS field was reading `undefined` because the decoder is JSON.parse passthrough and the wire emits a different key, the wire-canonical name is added and the orphan field is marked `@deprecated` (kept for compile-time compat; removed in v7). Per-type changes (full detail in CHANGELOG): Pure additions (Cat B): - WebhookSubscription.{secret, org_id, tenant_id} — security-critical; `secret` is the HMAC-SHA256 signing key required to verify inbound webhook payload signatures. - StepGateRequest.{cost_usd, tokens_in, tokens_out} - StepGateResponse.decision_id - ListWorkflowsResponse.{limit, offset} - StaticPolicy.{policy_id, priority, has_override} - CreateStaticPolicyRequest.{priority, tags} - UpdateStaticPolicyRequest.{priority, tags} - UpdatePlanRequest.metadata - UsageBreakdownItem.group_by - BudgetAlert.acknowledged - Budget.{org_id, tenant_id} - WorkflowStatusResponse.metadata - ExecutionSnapshot.retryCount - Finding.article RENAME_SAFE (orphan reads → wire-aligned canonical, old marked @deprecated): - StepGateResponse.policies_evaluated/policies_matched (snake) + policiesEvaluated/policiesMatched @deprecated - StaticPolicy.has_override (canonical) + hasOverride @deprecated - DynamicPolicyMatch.message + reason @deprecated - ExfiltrationCheckInfo.{exceeded, limit_type} + within_limits @deprecated - PolicyOverride.{id, enabled_override} + active @deprecated - PolicyVersion.{id, policy_id, change_summary, snapshot} + {changeDescription, previousValues, newValues} @deprecated - CancelPlanResponse.success + message @deprecated - CreateWorkflowResponse.started_at + {created_at, source} @deprecated - UsageRecord.{created_at, success, error_message, latency_ms, team_id, tenant_id, user_id, workflow_id} + timestamp @deprecated - ResumePlanResponse.result + 5 declared-but-never-populated fields marked @deprecated (workflowId, message, stepResult, nextStep, nextStepName, totalSteps) - PolicyEvaluationResult.{required_actions, processing_time_ms, database_accessed} (snake) + camelCase forms @deprecated UNRECOVERABLE realignments (no decoder; safe to add wire shape): - EffectivePoliciesResponse: tier-stratified wire shape ({static, dynamic, tenant_id, organization_id, computed_at}) - Policy: rich wire shape (policy_id, category, tier, pattern, severity, action, actions, conditions, description, organization_id, tenant_id, created_at/_by, updated_at/_by, version) — was exposing only 5 of 21 wire fields - PlanResponse: wire top-level fields (success, version, result, error, workflow_execution_id, policy_info) Transformer updates: - BudgetAlert decoder propagates acknowledged - UsageBreakdownItem decoder propagates groupBy - ExecutionSnapshot decoder propagates retryCount Wire-shape baseline regenerated: - 41 → 34 drift entries - All Cat B types fully resolved (RESOLVED ✓) - Remaining entries are all SDK-only fields (mostly @deprecated aliases retained for type-compat + Plugin Batch 1 SDK additions pending platform-side spec coverage) Filed alongside (no SDK change needed; spec is wrong): - axonflow-enterprise#1708 — AISystemRegistry.materiality_classification - axonflow-enterprise#1709 — DynamicPolicyInfo schema wrong shape Tests: 859 pass. Lint clean. * fix(client): propagate v6 fields through transformers (read + write) Companion to the type-definition sweep in this PR. The type updates added wire-canonical fields, but the hand-rolled transformers in client.ts weren't updated in parallel — so the fields were unreachable at runtime even though TypeScript types said they existed. The 5 reported gaps + 4 systematic-audit-found gaps = 9 fixes: Read-path fixes (decoders that dropped new wire fields): - generatePlan: PlanResponse now carries the wire top-level fields (success, version, result, error, workflow_execution_id, policy_info), sourced from agentResponse.* with data.* fallback. - resumePlan: ResumePlanResponse.result now populated from data.result. The deprecated `message` alias remains populated for back-compat. - getStaticPolicyVersions: inline-typed wire shape now declares the canonical fields (id, policy_id, change_summary, snapshot) alongside the legacy `change_description`/`previous_values`/ `new_values` keys; the transformer emits both halves. - listUsageRecords: UsageRecord transformer now emits the 8 new canonical fields (created_at, success, error_message, latency_ms, team_id, tenant_id, user_id, workflow_id). Legacy `timestamp` still emitted (it has always been undefined; both kept for source compat). - getExecution: inline step shape now declares retry_count, and the per-step mapper emits retryCount. (getExecutionSteps was already correct; only this method was out of sync.) - mapBudgetResponse: emits tenant_id, org_id (used by every Budget- returning method). - mapFindingResponse: emits article (used by mapAssessmentResponse). Write-path fixes (request builders that dropped new SDK fields): - updatePlan: body now includes metadata when set on the request. - createStaticPolicy: requestBody now includes priority, tags when set on the request. Verified clean (no gap): updateStaticPolicy (passthrough; field names match wire), stepGate (passthrough; new tokens_in/tokens_out/ cost_usd field names match wire), every method that flows through orchestratorRequest<T>/policyRequest<T> with a typed cast (the JSON parses naturally into the typed shape, so new fields appear without explicit transformer code). Meta-pattern observed: any method with a hand-rolled mapper helper or an explicit return-literal builder is a candidate gap when types gain fields. Methods that pass through to orchestratorRequest<T> inherit the new fields for free. The audit covered every type touched in the sweep across both directions. Tests: 859 pass. Lint clean. * fix(client): cancelPlan now propagates the canonical success field Same class as the 5 transformer gaps fixed in commit 2fac16f. The type-update added CancelPlanResponse.success but the cancelPlan() method's return literal didn't include it. The deprecated `message` slot is also kept populated so existing callers that read it see no behavior change (it has always been undefined against current servers, but zero-clobber). The systematic audit clearly missed cancelPlan in the previous round. The expanded sweep below documents every type/method pair to keep the contract honest: Read-path verifications (every method that returns a touched type): - BudgetAlert via getBudgetAlerts: acknowledged emitted ✓ (round 2) - UsageBreakdownItem via getUsageBreakdown: groupBy emitted ✓ (round 2) - ExecutionSnapshot via getExecutionSteps + getExecution: retryCount emitted ✓ (round 2) - PolicyEvaluationResult: embedded in PlanResponse.policy_info populated by generatePlan ✓ (round 2) - PlanResponse via generatePlan: 6 wire top-level fields ✓ (round 2) - CancelPlanResponse via cancelPlan: success now emitted (this commit) - ResumePlanResponse via resumePlan: result emitted ✓ (round 2) - StaticPolicy via list/get/create/update/toggle/getEffective: passthrough through policyRequest<T> ✓ - PolicyOverride via createPolicyOverride/listPolicyOverrides: passthrough through policyRequest<T> ✓ - PolicyVersion via getStaticPolicyVersions: id, policy_id, change_summary, snapshot emitted ✓ (round 2) - WorkflowStatusResponse via getWorkflow: passthrough ✓ - ListWorkflowsResponse via listWorkflows: passthrough ✓ - WebhookSubscription via createWebhook/getWebhook/updateWebhook: passthrough ✓ - ExfiltrationCheckInfo, DynamicPolicyMatch: embedded in MCP response shapes; passthrough ✓ - Finding via mapFindingResponse: article emitted ✓ (round 2) - Budget via mapBudgetResponse (used by createBudget/getBudget/ listBudgets/updateBudget/checkBudget/getBudgetStatus): tenant_id + org_id emitted ✓ (round 2) - UsageRecord via listUsageRecords: 8 wire fields emitted ✓ (round 2) - StepGateResponse via stepGate: passthrough; decision_id + policies_evaluated + policies_matched flow through ✓ - CreateWorkflowResponse via createWorkflow: passthrough; started_at flows through ✓ Write-path verifications (every method that accepts a touched request type): - StepGateRequest via stepGate: JSON.stringify passthrough; new cost_usd / tokens_in / tokens_out names match wire ✓ - UpdatePlanRequest via updatePlan: metadata in body builder ✓ (round 2) - CreateStaticPolicyRequest via createStaticPolicy: priority + tags in body builder ✓ (round 2) - UpdateStaticPolicyRequest via updateStaticPolicy: passthrough; field names match wire ✓ Falsey-clobber audit: 0 || patterns introduced in this PR's client.ts diff (verified with git diff grep). The transformer code uses ?? semantics or explicit guards. Tests: 859 pass. Lint clean. * chore(transformer-coverage): refresh baseline after v6 sweep burns down resumePlan PR #185's transformer fix populated all 5 fields previously missing on ResumePlanResponse (nextStep, nextStepName, stepResult, totalSteps, workflowId). Drop the stale baseline entry; remaining entries are executePlan and getPlanStatus on PolicyEvaluationResult, untouched this round.
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
Adds the same
validate-version-alignmentCI gate that just landed in axonflow-sdk-go (PR #130) and axonflow-sdk-python (PR #153). On the first local run, the gate surfaced a real drift:package-lock.jsonwas still at 5.5.0 whilepackage.jsonandsrc/version.tshad moved to 5.6.0 — fixed here vianpm install --package-lock-only.What's in this PR
validate-version-alignmentCI gate — new workflow + script. Fails any PR or push to main that leavespackage.json::version,src/version.ts::VERSION, orpackage-lock.json::versionout of sync with the most recent## [X.Y.Z]section in CHANGELOG.md.npm install --package-lock-onlyso it matchespackage.jsonat 5.6.0. This was real drift the new gate caught.Invariant on main
What's different for TS vs Go/Python
npm version X.Y.Z --no-git-tag-version+npm run stamp-version, which handles all three in two commands.python3 -c "import json..."for JSON parsing (python3 is available on all Ubuntu CI runners).Pre-fix diff caught by the gate
Test plan