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
- 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).
- Observe
artifacts/financial-account/contract.json's version field increments each run even though checksum/contractChecksum stays identical.
- Observe
contract.mcp.json's version field lags exactly one bump behind contract.json's.
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
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 onfinancial-accountinetendo_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: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'scontract-changelog.jsoninetendo_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-sensitiveJSON.stringifycomparison underneath).Bug 2 — contract.mcp.json never receives the version bump
In
cli/src/regen-all.js(and equivalentlycli/src/pipeline.js):contract.mcp.jsonis written using the in-memory contract's version before any version check runs.checkVersion()runs afterward, and if it detects a diff, bumps and rewritescontract.json's version in place — but never touchescontract.mcp.json.Result: any time
checkVersionbumps the version,contract.jsonandcontract.mcp.jsonend 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
etendo_schema_forge, run the pipeline twice in a row for a window whose contract hasenumValues[].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).artifacts/financial-account/contract.json'sversionfield increments each run even thoughchecksum/contractChecksumstays identical.contract.mcp.json'sversionfield lags exactly one bump behindcontract.json's.git statusnever goes clean — the "Verify no UI / contract drift" CI step fails indefinitely.Expected behavior
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-sensitiveJSON.stringifycomparison; needs a canonical (key-sorted) stringify or structural deep-equal instead.cli/src/check-version.js(checkVersion, ~line 287) — bumpscontract.jsononly; never updatescontract.mcp.json.cli/src/regen-all.js(~line 193) /cli/src/pipeline.js(~line 706) — writescontract.mcp.jsonbeforecheckVersionruns, guaranteeing staleness whenever a bump fires.Proposed fix
cli/src/check-version.js(diffFields, ~line 42) — replace rawJSON.stringifywith a canonical/key-sorted stringify so property-order noise stops triggering false positives:(
canonicalStringify= a small helper that recursively sorts object keys beforeJSON.stringify, e.g. via a stable replacer function passed toJSON.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 bumpingcontract.json, also synccontract.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.jscould be reordered socontract.mcp.jsonis written aftercheckVersionruns, using the already-bumpedcontract.jsonas the source of truth — but patchingcheckVersiondirectly 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