Skip to content

ci+chore: version-alignment gate + resync package-lock to 5.6.0#182

Merged
saurabhjain1592 merged 3 commits into
mainfrom
chore/version-alignment-ci
Apr 25, 2026
Merged

ci+chore: version-alignment gate + resync package-lock to 5.6.0#182
saurabhjain1592 merged 3 commits into
mainfrom
chore/version-alignment-ci

Conversation

@saurabhjain1592

Copy link
Copy Markdown
Member

Summary

Adds the same validate-version-alignment CI 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.json was still at 5.5.0 while package.json and src/version.ts had moved to 5.6.0 — fixed here via npm install --package-lock-only.

What's in this PR

  1. validate-version-alignment CI gate — new workflow + script. Fails any PR or push to main that leaves package.json::version, src/version.ts::VERSION, or package-lock.json::version 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 the new gate caught.

Invariant on main

package.json::version
  == src/version.ts::VERSION
  == package-lock.json::version
  == first ## [X.Y.Z] section in CHANGELOG.md

What's different for TS vs Go/Python

  • Three manifest files to track (package.json + src/version.ts + package-lock.json) instead of one or two.
  • Remediation hint in the error message points at npm version X.Y.Z --no-git-tag-version + npm run stamp-version, which handles all three in two commands.
  • Validator uses python3 -c "import json..." for JSON parsing (python3 is available on all Ubuntu CI runners).

Pre-fix diff caught by the gate

📋 Latest CHANGELOG version: 5.6.0

📦 Checking package.json...
  ✅ package.json — 5.6.0
🔧 Checking src/version.ts...
  ✅ src/version.ts — 5.6.0
🔒 Checking package-lock.json...
  ❌ package-lock.json — version is "5.5.0", expected "5.6.0"

❌ Found 1 version misalignment(s).

Test plan

  • Gate fires on drift, with clear remediation
  • Gate passes when aligned
  • CI green on this PR

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
@saurabhjain1592 saurabhjain1592 merged commit 92ca3b7 into main Apr 25, 2026
9 checks passed
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.
@saurabhjain1592 saurabhjain1592 deleted the chore/version-alignment-ci branch April 26, 2026 10:08
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