MemberJunction / MemberJunction/MJ
CI: repo-wide compliance scanners in MJGlobal are replayed from turbo cache — next is latently red (9 MetadataSync violations)
- Dominant language
- TSQL
- Stars
- 29
- Forks
- 6
- Avg merge
- 2d 1h
- Merged PRs (30d)
- 323
Description
## Summary
`next` has two failing repo-wide compliance scanners that CI reports as **green**, because turbo replays MJGlobal's cached `test` result instead of running it. The scanners read source files **outside** MJGlobal, which turbo's cache key does not track. PR #4343 is the first to touch MJGlobal since the violations landed, so it is the first run to actually execute the scanners — and it went red for code it does not touch.
## Symptom
On PR #4343 @ a0bcd1f6f9, `Unit tests (shard 3/6)` fails with:
- `MJGlobal/src/__tests__/UUIDCompliance.test.ts` — "Found 7 direct UUID comparison(s) that should use UUIDsEqual()"
- `MJGlobal/src/__tests__/MultiProviderCompliance.test.ts` — "Found 2 non-allowlisted global-provider reference(s)"
All nine flagged lines are in **MetadataSync**, unchanged by that PR:
```
MetadataSync/src/lib/RecordProcessor.ts:709 const childSubtypes = Metadata.Provider?.Entities
MetadataSync/src/lib/RecordProcessor.ts:710 ? Metadata.Provider.Entities.filter(
MetadataSync/src/lib/RecordProcessor.ts:711 (e) => e.ParentID === record.EntityInfo.ID || e.ParentEntityInfo?.ID === record.EntityInfo.ID
MetadataSync/src/services/ValidationService.ts:379 relatedEntityInfo.ParentID === entityInfo.ID ||
MetadataSync/src/services/ValidationService.ts:1703 (e) => e.ParentID === parentEntityInfo.ID || e.ParentEntityInfo?.ID === parentEntityInfo.ID
MetadataSync/src/services/ValidationService.ts:1745 childEntityInfo.ParentID === parentEntityInfo.ID ||
MetadataSync/src/services/ValidationService.ts:1746 childEntityInfo.ParentEntityInfo?.ID === parentEntityInfo.ID;
MetadataSync/src/services/ValidationService.ts:1785 childEntityInfo.ParentID === parentEntityInfo.ID ||
MetadataSync/src/services/ValidationService.ts:1786 childEntityInfo.ParentEntityInfo?.ID === parentEntityInfo.ID;
```
Introduced by d49a613137 (`feat(sync): first-class composition axes …`, 2026-09-08).
## Evidence that `next` is latently red
- Checking out pristine `origin/next` @ 6ac6bcc5ca and running the two tests locally: **both fail** with the same nine findings.
- `next`'s "green" Unit Tests run 34505883946 (9b9e5a4ba9), shard 4, job 102971424567, logs for `@memberjunction/global:test`:
```
##[group]@memberjunction/global:test
cache hit, replaying logs a0c866c68423a1a8
```
The ✓ for `UUIDCompliance.test.ts` / `MultiProviderCompliance.test.ts` in that log is a replay, not an execution.
- PR run 34511527823 (a0bcd1f6f9), job 102988891135: MJGlobal changed → cache miss → scanners executed → red.
## Two things to fix
1. **The nine MetadataSync lines.** A verified fix exists (scanners green, MetadataSync 364/364, `tsc` clean). Note the `RecordProcessor` read must stay null-safe — `sync-composition-axes.test.ts` reaches it with no provider registered, and a plain `new Metadata()` throws there — so it keeps `Metadata.Provider` with the same `// global-provider-ok: MetadataSync is a single-provider CLI process` note the file already uses at line 784. Patch below.
2. **The cache blind spot.** Any test that scans the repo (these two, and likely others in MJGlobal such as the codegen/registration guards) must not be a cached turbo task keyed only on its own package. Options: declare the scanned tree as `inputs` for `@memberjunction/global#test` in `turbo.json`, or move repo-wide scanners into a dedicated non-cached "source guards" job (there is already a `Source guards` job in `test.yml`). Until then every "green" on `next` for these scanners is unproven.
## Patch for (1)
```diff
diff --git a/packages/MetadataSync/src/lib/RecordProcessor.ts b/packages/MetadataSync/src/lib/RecordProcessor.ts
index c8361134d5..402f3ab52b 100644
--- a/packages/MetadataSync/src/lib/RecordProcessor.ts
+++ b/packages/MetadataSync/src/lib/RecordProcessor.ts
@@ -1,5 +1,5 @@
import { BaseEntity, CompositeKey, EntityInfo, Metadata, RunView, UserInfo } from '@memberjunction/core';
-import { ordinalCompare } from '@memberjunction/global';
+import { ordinalCompare, UUIDsEqual } from '@memberjunction/global';
import { SyncEngine, RecordData } from '../lib/sync-engine';
import { EntityConfig } from '../config';
import { JsonWriteHelper } from './json-write-helper';
@@ -706,9 +706,12 @@ export class RecordProcessor {
}
// Determine if child type is ambiguous (more than one subtype exists in metadata)
- const childSubtypes = Metadata.Provider?.Entities
- ? Metadata.Provider.Entities.filter(
- (e) => e.ParentID === record.EntityInfo.ID || e.ParentEntityInfo?.ID === record.EntityInfo.ID
+ // Null-safe on purpose: callers (and the composition-axes tests) reach here with no provider
+ // registered, and an unresolvable subtype set must mean "not ambiguous", not a throw.
+ const globalProvider = Metadata.Provider; // global-provider-ok: MetadataSync is a single-provider CLI process
+ const childSubtypes = globalProvider?.Entities
+ ? globalProvider.Entities.filter(
+ (e) => UUIDsEqual(e.ParentID, record.EntityInfo.ID) || UUIDsEqual(e.ParentEntityInfo?.ID, record.EntityInfo.ID)
)
: [];
const needsEntityName = childSubtypes.length > 1 || record.EntityInfo.AllowMultipleSubtypes;
diff --git a/packages/MetadataSync/src/services/ValidationService.ts b/packages/MetadataSync/src/services/ValidationService.ts
index 7d9915629a..97a86db5bd 100644
--- a/packages/MetadataSync/src/services/ValidationService.ts
+++ b/packages/MetadataSync/src/services/ValidationService.ts
@@ -1,3 +1,4 @@
+import { UUIDsEqual } from '@memberjunction/global';
import { EntityFieldInfo, EntityInfo, EntityRelationshipInfo, Metadata, RunView } from '@memberjunction/core';
import * as fs from 'fs';
import * as path from 'path';
@@ -376,7 +377,7 @@ export class ValidationService {
// Section 4.5 diagnostics:
// Check if related entity looks like an IsA child (shared PK / ParentID is this entity)
if (
- relatedEntityInfo.ParentID === entityInfo.ID ||
+ UUIDsEqual(relatedEntityInfo.ParentID, entityInfo.ID) ||
(relatedEntityInfo.ParentEntityInfo && relatedEntityInfo.ParentEntityInfo.Name.trim().toLowerCase() === entityInfo.Name.trim().toLowerCase())
) {
this.addWarning({
@@ -1700,7 +1701,7 @@ export class ValidationService {
} else {
// Look up registered subtypes
const childSubtypes = this.metadata.Entities.filter(
- (e) => e.ParentID === parentEntityInfo.ID || e.ParentEntityInfo?.ID === parentEntityInfo.ID
+ (e) => UUIDsEqual(e.ParentID, parentEntityInfo.ID) || UUIDsEqual(e.ParentEntityInfo?.ID, parentEntityInfo.ID)
);
if (childSubtypes.length === 1) {
childEntityName = childSubtypes[0].Name;
@@ -1742,8 +1743,8 @@ export class ValidationService {
}
const isSubtype =
- childEntityInfo.ParentID === parentEntityInfo.ID ||
- childEntityInfo.ParentEntityInfo?.ID === parentEntityInfo.ID;
+ UUIDsEqual(childEntityInfo.ParentID, parentEntityInfo.ID) ||
+ UUIDsEqual(childEntityInfo.ParentEntityInfo?.ID, parentEntityInfo.ID);
if (!isSubtype) {
this.addError({
type: 'entity',
@@ -1782,8 +1783,8 @@ export class ValidationService {
}
const isSubtype =
- childEntityInfo.ParentID === parentEntityInfo.ID ||
- childEntityInfo.ParentEntityInfo?.ID === parentEntityInfo.ID;
+ UUIDsEqual(childEntityInfo.ParentID, parentEntityInfo.ID) ||
+ UUIDsEqual(childEntityInfo.ParentEntityInfo?.ID, parentEntityInfo.ID);
if (!isSubtype) {
this.addError({
type: 'entity',
```
Found while landing #4343 (which does not touch MetadataSync).
Contributor guide
Assessment
This issue has not been assessed yet.