Skip to content

Version-check false-positive diff desyncs contract.mcp.json #30

Description

@Gremiger

Description

The version-tracking mechanism (cli/src/check-version.js) has two related bugs that make the pipeline non-idempotent and permanently break the offline-regen-check CI gate for any window that hits this path — confirmed on financial-account in etendo_schema_forge (PR #851).

This affects any window whose contract fields carry object-valued or array-valued properties (e.g. enumValues[].labels, locale translation maps) — it is not specific to that window's data, it's a property of the diffing algorithm itself.

Bug 1 — Order-sensitive diff produces false-positive version bumps

diffFields() compares field properties via:

if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {

This is order-sensitive. Object-valued properties can be constructed with different key/array ordering across independent pipeline runs even when the underlying data is semantically identical, causing the diff to report a spurious "change" on every run. Confirmed via financial-account's contract-changelog.json in etendo_schema_forge, which shows version bumps on 3 consecutive days with reason text that looks identical on both sides ("enumValues changed from [object Object],... to [object Object],..." — the display text hides the real, order-sensitive JSON.stringify comparison underneath).

Bug 2 — contract.mcp.json never receives the version bump

In cli/src/regen-all.js (and equivalently cli/src/pipeline.js):

  • contract.mcp.json is written using the in-memory contract's version before any version check runs.
  • checkVersion() runs afterward, and if it detects a diff, bumps and rewrites contract.json's version in place — but never touches contract.mcp.json.

Result: any time checkVersion bumps the version, contract.json and contract.mcp.json end up exactly one version apart, permanently — and this regenerates itself identically on every subsequent pipeline run, not just once. It is not a stale-commit problem; it is self-inflicted on every invocation, including CI's own regen-check run. Combined with Bug 1's spurious triggering, this makes the offline-regen-check CI gate impossible to satisfy for the affected window without a source fix — no amount of re-committing generated output resolves it.

Steps to reproduce

  1. In etendo_schema_forge, run the pipeline twice in a row for a window whose contract has enumValues[].labels (e.g. make regen-check FROM_CACHE=1 ONLY=financial-account REGEN_CHECK_PREV_XML_DIR=../modules/com.etendoerp.go/src-db/database/sourcedata).
  2. Observe artifacts/financial-account/contract.json's version field increments each run even though checksum/contractChecksum stays identical.
  3. Observe contract.mcp.json's version field lags exactly one bump behind contract.json's.
  4. git status never goes clean — the "Verify no UI / contract drift" CI step fails indefinitely.

Expected behavior

  • Running the pipeline twice with no real data changes should be idempotent: no version bump, no diff, clean working tree.
  • When a real version bump does legitimately occur, contract.mcp.json's version should be updated in the same operation so the two files never desync.

Affected components

  • cli/src/check-version.js (diffFields, line ~42) — order-sensitive JSON.stringify comparison; needs a canonical (key-sorted) stringify or structural deep-equal instead.
  • cli/src/check-version.js (checkVersion, ~line 287) — bumps contract.json only; never updates contract.mcp.json.
  • cli/src/regen-all.js (~line 193) / cli/src/pipeline.js (~line 706) — writes contract.mcp.json before checkVersion runs, guaranteeing staleness whenever a bump fires.

Proposed fix

cli/src/check-version.js (diffFields, ~line 42) — replace raw JSON.stringify with a canonical/key-sorted stringify so property-order noise stops triggering false positives:

-      if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
+      if (canonicalStringify(oldVal) !== canonicalStringify(newVal)) {

(canonicalStringify = a small helper that recursively sorts object keys before JSON.stringify, e.g. via a stable replacer function passed to JSON.stringify(value, (k, v) => v && typeof v === 'object' && !Array.isArray(v) ? Object.keys(v).sort().reduce((o, k) => (o[k] = v[k], o), {}) : v).)

cli/src/check-version.js (checkVersion, ~line 287-291) — after bumping contract.json, also sync contract.mcp.json's version:

   currentContract.version = newVersion;
   await writeFile(
     join(artifactDir, 'contract.json'),
     JSON.stringify(currentContract, null, 2) + '\n'
   );
+  try {
+    const mcpRaw = await readFile(join(artifactDir, 'contract.mcp.json'), 'utf-8');
+    const mcpContract = JSON.parse(mcpRaw);
+    mcpContract.version = newVersion;
+    await writeFile(join(artifactDir, 'contract.mcp.json'), JSON.stringify(mcpContract, null, 2) + '\n');
+  } catch { /* no mcp split for this window */ }

This is a long fix (root cause spans two files) — the diff above shows the key changed lines; the surrounding function bodies are otherwise unchanged. Alternatively/additionally, regen-all.js/pipeline.js could be reordered so contract.mcp.json is written after checkVersion runs, using the already-bumped contract.json as the source of truth — but patching checkVersion directly is more robust since it also covers any other caller.

Reference

Jira: ETP-4458
Discovered while resolving ETP-4452, PR etendosoftware/etendo_schema_forge#851

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions