diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/unique-name-properties.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/unique-name-properties.ts index 45d2a9cd9..94ab2168c 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/unique-name-properties.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/unique-name-properties.ts @@ -101,6 +101,19 @@ const OVERSATURATED = null; interface PropertyCandidate { readonly id: string; readonly filePath: string; + /** + * True when this definition is the RETURN SHAPE of a function (R3-4) rather + * than a declared surface — a named object literal, a class field, an + * interface or alias member. + * + * Return shapes are the weaker anchor and are ranked below declared ones, so + * adding them cannot change an answer that already resolved. That is what + * reconciles this with R2-1b, which deliberately modelled returned keys as + * WRITES to avoid adding same-named competitors to narrowing: they are + * definitions now, but they never outrank a real declaration, so the + * competitor problem it was avoiding does not come back. + */ + readonly fromReturnShape: boolean; } /** @@ -186,13 +199,18 @@ export function buildPropertyNameIndex(graph: KnowledgeGraph): PropertyNameIndex if (typeof name !== 'string' || name.length === 0) continue; const filePath = node.properties.filePath; if (typeof filePath !== 'string') continue; + const candidate: PropertyCandidate = { + id: node.id, + filePath, + fromReturnShape: node.properties.fromReturnShape === true, + }; const existing = byName.get(name); if (existing === undefined) { - byName.set(name, [{ id: node.id, filePath }]); + byName.set(name, [candidate]); continue; } if (existing.some((c) => c.id === node.id)) continue; - existing.push({ id: node.id, filePath }); + existing.push(candidate); } return byName; } @@ -281,13 +299,24 @@ function buildDirectImportMap( * imported third would answer a question the reader's own file contradicts. */ function narrowToSingleCandidate( - candidates: readonly PropertyCandidate[], + candidatesIn: readonly PropertyCandidate[], readingFile: string, importedFiles: ReadonlySet | undefined, ): { readonly id: string; readonly tier: string } | null { - if (candidates.length === 1) { - return { id: candidates[0]!.id, tier: 'workspace-unique' }; + // DECLARED ANCHORS FIRST. A return shape is a real definition but a weaker + // one: it says "some function builds an object with this key", where a named + // literal or a class/interface member says "this IS the field". Whenever both + // exist, the declared one is what a reader means — and ranking it first is + // what guarantees R3-4 cannot change an answer that already resolved before + // return shapes were indexed at all. + let candidates = candidatesIn; + const declared = candidates.filter((c) => !c.fromReturnShape); + const ranked = declared.length > 0 ? declared : candidates; + + if (ranked.length === 1) { + return { id: ranked[0]!.id, tier: 'workspace-unique' }; } + candidates = ranked; const sameFile = candidates.filter((c) => c.filePath === readingFile); if (sameFile.length > 0) { diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index f2655c4a8..7a6dee3df 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -424,6 +424,43 @@ export const TYPESCRIPT_QUERIES = ` (pair key: (property_identifier) @name) @definition.property)) +; Keys of an ANONYMOUS object literal in RETURN position (R3-4). The dominant +; shape in idiomatic JS: 437 sites in one backend directory of the reporting +; repo, including the ~25-field payload of its whole signal pipeline, none of +; which could be named because the literal binds to nothing. +; +; The enclosing function is the owner -- the literal is that function's return +; shape, a contract its callers consume -- so the key qualifies as +; . and two functions returning the same key stay distinct. +; +; DEFINITIONS, unlike the record-construction writes of R2-1b, and the +; difference is deliberate: there a definition already existed elsewhere and a +; construction site was a USE of it, while here nothing else names the field at +; all. To keep that from regressing R2-1b's case, narrowing ranks declared +; anchors ABOVE return shapes, so a name that already resolves keeps resolving +; to what it resolved to before. +(return_statement + (object + (pair + key: (property_identifier) @name) @definition.property)) + +; SHORTHAND keys of the same literal. "return { symbol, interval, score }" is +; the commonest spelling of all -- the reporting repo's own alert payload is +; mostly shorthand -- and (pair) does not match it: tree-sitter models it as +; shorthand_property_identifier, where the key IS the value. Found by dumping +; the golden fixture and noticing that a literal returning +; { level, message, timestamp: Date.now() } had indexed only timestamp. +(return_statement + (object + (shorthand_property_identifier) @name @definition.property)) + +; Shorthand keys of a named object literal -- same gap, same reason as the +; return-position rule above. +(variable_declarator + name: (identifier) + value: (object + (shorthand_property_identifier) @name @definition.property)) + (variable_declarator name: (identifier) value: (call_expression @@ -932,6 +969,43 @@ export const JAVASCRIPT_QUERIES = ` (pair key: (property_identifier) @name) @definition.property)) +; Keys of an ANONYMOUS object literal in RETURN position (R3-4). The dominant +; shape in idiomatic JS: 437 sites in one backend directory of the reporting +; repo, including the ~25-field payload of its whole signal pipeline, none of +; which could be named because the literal binds to nothing. +; +; The enclosing function is the owner -- the literal is that function's return +; shape, a contract its callers consume -- so the key qualifies as +; . and two functions returning the same key stay distinct. +; +; DEFINITIONS, unlike the record-construction writes of R2-1b, and the +; difference is deliberate: there a definition already existed elsewhere and a +; construction site was a USE of it, while here nothing else names the field at +; all. To keep that from regressing R2-1b's case, narrowing ranks declared +; anchors ABOVE return shapes, so a name that already resolves keeps resolving +; to what it resolved to before. +(return_statement + (object + (pair + key: (property_identifier) @name) @definition.property)) + +; SHORTHAND keys of the same literal. "return { symbol, interval, score }" is +; the commonest spelling of all -- the reporting repo's own alert payload is +; mostly shorthand -- and (pair) does not match it: tree-sitter models it as +; shorthand_property_identifier, where the key IS the value. Found by dumping +; the golden fixture and noticing that a literal returning +; { level, message, timestamp: Date.now() } had indexed only timestamp. +(return_statement + (object + (shorthand_property_identifier) @name @definition.property)) + +; Shorthand keys of a named object literal -- same gap, same reason as the +; return-position rule above. +(variable_declarator + name: (identifier) + value: (object + (shorthand_property_identifier) @name @definition.property)) + ; Same named shape, behind an IDENTITY-PRESERVING wrapper (R2-1a): ; ; export const INERT_EXIT_CONTRACT = Object.freeze({ exitModel: 'bracket', ... }); diff --git a/gitnexus/src/core/ingestion/utils/ast-helpers.ts b/gitnexus/src/core/ingestion/utils/ast-helpers.ts index f4ec2dd2f..65f06bd31 100644 --- a/gitnexus/src/core/ingestion/utils/ast-helpers.ts +++ b/gitnexus/src/core/ingestion/utils/ast-helpers.ts @@ -1177,6 +1177,91 @@ const BLOCK_SCOPE_BOUNDARY_TYPES = new Set([ * ancestor also returns null (catches block-scoped declarations inside * top-level `if`/`for`/`try`/etc., which cannot be imported). */ +/** + * Owner for the keys of an ANONYMOUS object literal in return position (R3-4). + * + * `return { symbol, score, wickRatio, … }` binds to nothing, so its keys had no + * anchor and could not be qualified — which on the reporting repo left the + * central payload of the signal pipeline, ~25 fields, entirely unqueryable. + * There are 437 such sites in one backend directory, so this is the dominant + * shape, not an edge case. + * + * The enclosing FUNCTION is the honest owner: the literal is that function's + * return shape, which is a contract its callers consume. Qualifying by it keeps + * two functions returning the same key name as two distinct nodes, exactly as + * `ownerName` does for variable-bound literals. + * + * Returns null when the literal is not DIRECTLY returned (a nested literal, or + * one inside a callback several frames down), because then the enclosing + * function is not what the object describes. + */ +/** + * True when this definition node is a key of a literal in RETURN position. + * + * Deliberately independent of whether an OWNER NAME could be derived. The two + * are different questions, and conflating them mislabels the anonymous case: + * `[function (row) { return { k: row.x }; }]` yields no name to qualify by, so + * the owner lookup returns null — but the key is still a return shape, and + * flagging it by owner-presence would leave it looking like a DECLARED anchor + * and let it outrank a real declaration during narrowing. + */ +export const isReturnShapeProperty = (node: SyntaxNode): boolean => { + let current: SyntaxNode | null = node; + let objectDepth = 0; + while (current && objectDepth === 0) { + if (current.type === 'object') objectDepth = 1; + else if (FUNCTION_NODE_TYPES.has(current.type)) return false; + else current = current.parent; + } + return current?.parent?.type === 'return_statement'; +}; + +export const findReturnShapeOwnerInfo = ( + node: SyntaxNode, + filePath: string, + // NO `ownerId`, deliberately, and the union's optional field is what says so. + // An owner id would emit `HAS_PROPERTY` from the FUNCTION, a `Function|Property` + // relation pair that the schema does not declare — and an undeclared pair does + // not degrade, it throws `UndeclaredRelationPairError` and kills the entire + // analyze. That already shipped once in this PR. The qualifier alone is what + // this needs: it makes the key nameable and keeps two functions' same-named + // keys distinct, without asserting a containment edge nothing consumes. +): { readonly ownerId?: string; readonly ownerName: string } | null => { + // Walk to the literal this key belongs to; bail if it is nested inside + // another object, whose shape it describes instead. + let current: SyntaxNode | null = node; + let objectDepth = 0; + while (current && objectDepth === 0) { + if (current.type === 'object') objectDepth = 1; + else if (FUNCTION_NODE_TYPES.has(current.type)) return null; + else current = current.parent; + } + if (!current) return null; + const literal = current; + if (literal.parent?.type !== 'return_statement') return null; + + // The nearest enclosing function-like, and its name. An anonymous function + // (a callback, an IIFE) gives nothing to qualify by, so those stay + // unanchored rather than colliding on a shared empty owner. + let fn: SyntaxNode | null = literal.parent.parent; + while (fn && !FUNCTION_NODE_TYPES.has(fn.type)) fn = fn.parent; + if (!fn) return null; + + const nameNode = fn.childForFieldName?.('name'); + if (nameNode?.type === 'identifier' || nameNode?.type === 'property_identifier') { + return { ownerName: nameNode.text }; + } + // `const formatAlert = (…) => ({ … })` and `const f = function () {}`: the + // name is on the declarator, not the function. + const declarator = fn.parent; + if (declarator?.type === 'variable_declarator') { + const declName = declarator.childForFieldName?.('name'); + if (declName?.type === 'identifier') return { ownerName: declName.text }; + } + void filePath; + return null; +}; + export const findObjectLiteralBindingInfo = ( node: SyntaxNode, filePath: string, diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 029ffab83..2158cffd4 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -91,6 +91,8 @@ import { getDefinitionNodeFromCaptures, findEnclosingClassInfo, findObjectLiteralBindingInfo, + findReturnShapeOwnerInfo, + isReturnShapeProperty, findMemberAssignmentOwnerInfo, isCjsDefaultExportAssignment, type EnclosingClassInfo, @@ -2361,8 +2363,19 @@ const processFileGroup = ( // byte-identical or every object-literal method in every indexed // repo changes id. includeOwnerName: nodeLabel === 'Property', - })) + }) ?? + // R3-4: an anonymous literal in return position is owned by the + // function whose shape it is. Last in the chain so a variable-bound + // literal keeps its existing owner and its existing id. + (nodeLabel === 'Property' ? findReturnShapeOwnerInfo(definitionNode, file.path) : null)) : null; + // Provenance for narrowing (R3-4). A return shape is a real definition but + // the weaker one, and the unique-name pass ranks declared anchors above it + // so indexing these cannot change an answer that already resolved. + const returnShapeProperty = + nodeLabel === 'Property' && definitionNode !== undefined && definitionNode !== null + ? isReturnShapeProperty(definitionNode) + : false; // #1978: hoisted ABOVE qualifiedName/node-id (load-bearing order) so a // class-like node can key its id by its fully-qualified path. Derived from @@ -2809,6 +2822,7 @@ const processFileGroup = ( ...(description !== undefined ? { description } : {}), ...methodProps, ...(declaredType !== undefined ? { declaredType } : {}), + ...(returnShapeProperty ? { fromReturnShape: true } : {}), }, }); diff --git a/gitnexus/src/storage/parse-cache.ts b/gitnexus/src/storage/parse-cache.ts index c798f97e5..0b8535df8 100644 --- a/gitnexus/src/storage/parse-cache.ts +++ b/gitnexus/src/storage/parse-cache.ts @@ -289,7 +289,18 @@ import type { ParseWorkerResult } from '../core/ingestion/workers/parse-worker.j // clashes. It is NOT on this branch, so until that one merges the re-check // below is still manual. // RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING. -const SCHEMA_BUMP = 48; +// 48 -> 49 for the return-shape and shorthand captures (R3-4): keys of an +// anonymous literal in return position, and shorthand keys in both that and the +// variable-bound form. Parse-time again. +// +// The v34 hazard, and this branch has already tripped it: a build stamped 48 +// was installed and used to analyze two repos BEFORE these captures existed, so +// caches stamped 48 exist that carry none of them. Within one PR the version +// only has to differ from main's, but an INTERMEDIATE build of the same series +// is a different capture set wearing the same number — which is exactly what +// the note above records for 33/34. +// RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING. +const SCHEMA_BUMP = 49; const GITNEXUS_PKG_VERSION = (() => { try { // package.json sits at gitnexus/package.json — two levels up from diff --git a/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/return-shape.js b/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/return-shape.js new file mode 100644 index 000000000..da8e5a2af --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/return-shape.js @@ -0,0 +1,47 @@ +// R3-4: an anonymous literal in return position — the dominant shape in +// idiomatic JS (437 sites in one backend directory of the reporting repo), +// including the ~25-field payload of its entire signal pipeline. It binds to +// nothing, so its keys had no anchor and could not be named at all. +export function formatAlert(row) { + const shorthandOnlyField = row.shorthand; + return { + returnShapeOnlyField: row.raw, + sharedWithDeclared: row.other, + // SHORTHAND — the commonest spelling, and the one `(pair)` cannot match. + // The reporting repo's own alert payload is mostly this form. + shorthandOnlyField, + }; +} + +// A SECOND function returning a same-named key. Two distinct shapes, so two +// distinct nodes — qualifying by the owning function is what keeps them apart. +export function formatSummary(row) { + return { + summaryOnlyField: row.summary, + }; +} + +// The reader. Untyped receiver, so this is the name-inference path. +export function readsReturnShape(alert) { + return alert.returnShapeOnlyField; +} + +// The R2-1b GUARANTEE, as a fixture: a DECLARED anchor for the same name. +// `sharedWithDeclared` is both a named-object key and a return-shape key, and a +// read of it must keep resolving to the DECLARED one — otherwise indexing +// return shapes would silently move existing answers. +export const declaredHome = { + sharedWithDeclared: 1, +}; + +export function readsShared(bag) { + return bag.sharedWithDeclared; +} + +// Anonymous functions give nothing to qualify by, so their return shapes stay +// unanchored rather than colliding on a shared empty owner. +export const anonHolder = [ + function (row) { + return { anonReturnKey: row.x }; + }, +]; diff --git a/gitnexus/test/fixtures/lang-resolution/typescript-alias-fields/contracts.ts b/gitnexus/test/fixtures/lang-resolution/typescript-alias-fields/contracts.ts index a7689757d..56657ac0e 100644 --- a/gitnexus/test/fixtures/lang-resolution/typescript-alias-fields/contracts.ts +++ b/gitnexus/test/fixtures/lang-resolution/typescript-alias-fields/contracts.ts @@ -44,6 +44,11 @@ export type NestedConfig = { }; // Inline RETURN type — the third position the unanchored rule reached. -export function buildInline(): { inlineReturnOnlyKey: number } { - return { inlineReturnOnlyKey: 1 }; +// The TYPE annotation's member and the returned VALUE's key are named +// differently ON PURPOSE. They are separate rules with opposite expectations — +// an inline return TYPE must mint nothing (RV-4), while a returned literal's +// keys are a function's return shape and must mint (R3-4) — and sharing a name +// left the RV-4 assertion unable to tell which rule produced the node. +export function buildInline(): { inlineReturnTypeOnlyKey: number } { + return { inlineReturnValueOnlyKey: 1 } as never; } diff --git a/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json b/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json index 4a5d5b408..14fa41596 100644 --- a/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json +++ b/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json @@ -2,8 +2,8 @@ "capture": "initial capture (U8, post-U1–U7)", "fixture": "mini-repo", "totalFileCount": 7, - "symbols": 41, - "relationships": 85, + "symbols": 51, + "relationships": 95, "processes": 4, "byType": { "Class": 1, @@ -14,13 +14,13 @@ "Interface": 3, "Method": 1, "Process": 4, - "Property": 8 + "Property": 18 }, "byRelType": { "ACCESSES": 3, "CALLS": 9, "CONTAINS": 7, - "DEFINES": 16, + "DEFINES": 26, "HAS_METHOD": 1, "HAS_PROPERTY": 8, "IMPORTS": 12, @@ -28,5 +28,5 @@ "STEP_IN_PROCESS": 12, "USES": 5 }, - "edgeDigest": "c55bd8307a5fccbfd23d5aa93f241e26ef0f45c1a9e57691f2bf4f6602e8e4ab" + "edgeDigest": "5dddd1f466deeda1197eb61b480a4f3a5da67dfc0d00ace22377158366e87d00" } diff --git a/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts b/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts index 43670f0ed..e7a72877e 100644 --- a/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts +++ b/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts @@ -145,11 +145,21 @@ describe('JavaScript plain-object property access (A1/A5)', () => { expect(writersOf('destructuredOnlyField')).toContain('buildFlat'); }); - // The point of modelling these as writes rather than definitions: more - // definitions would add same-named competitors to the narrowing that makes - // these fields resolvable in the first place. - it('mints NO new definition for a constructed record', () => { - expect(propertyNames().filter((n) => n === 'destructuredOnlyField')).toHaveLength(1); + // This asserted `toHaveLength(1)` — no new definition — until R3-4 began + // anchoring returned literals, which mints exactly one here (`buildFlat`'s + // return shape). The assertion was the right instinct expressed as the + // wrong invariant: what R2-1b actually protects is that adding definitions + // must not move an answer that already resolved, and node count was a proxy + // for that. The property itself is now asserted directly, and it holds + // because narrowing ranks declared anchors above return shapes. + it('keeps the DECLARED definition winning despite a return-shape sibling', () => { + const nodes = propertyNames().filter((n) => n === 'destructuredOnlyField'); + expect(nodes.length).toBeGreaterThan(1); + // Every reader still resolves, and to the declared home — a read that had + // dropped to ambiguous would show up as a missing edge here. + for (const reader of ['appliesDestructured', 'appliesShorthand', 'appliesRenamed']) { + expect(readersOf('destructuredOnlyField')).toContain(reader); + } }); it('leaves an inline call-argument prop bag alone', () => { @@ -158,6 +168,54 @@ describe('JavaScript plain-object property access (A1/A5)', () => { }); }); + // R3-4. The dominant shape in idiomatic JS and the one with no anchor at all: + // 437 `return {` sites in a single backend directory of the reporting repo, + // including the ~25-field payload of its whole signal pipeline. The literal + // binds to nothing, so its keys could not even be named. + describe('anonymous returned object literals (R3-4)', () => { + it('indexes keys of a literal returned from a named function', () => { + expect(propertyNames()).toContain('returnShapeOnlyField'); + }); + + it('resolves a read of a return-shape key', () => { + expect(readersOf('returnShapeOnlyField')).toContain('readsReturnShape'); + }); + + // `{ symbol, interval, score }` is the commonest spelling of all and + // `(pair)` does not match it — tree-sitter models it as + // `shorthand_property_identifier`, where the key IS the value. Caught by + // dumping the golden fixture and seeing that a literal returning + // `{ level, message, timestamp: Date.now() }` had indexed only `timestamp`. + it('indexes SHORTHAND keys, not just explicit pairs', () => { + expect(propertyNames()).toContain('shorthandOnlyField'); + }); + + // Qualified by the owning function, so two functions returning the same key + // are two shapes rather than one merged symbol — the same collision + // `ownerName` prevents for variable-bound literals. + it('qualifies by the owning function', () => { + const ids = Array.from( + (result as unknown as { graph: { iterNodes(): Iterable } }).graph.iterNodes(), + ) + .filter((n) => n.label === 'Property') + .map((n) => String(n.id)); + expect(ids.some((id) => id.includes('formatAlert.returnShapeOnlyField'))).toBe(true); + expect(ids.some((id) => id.includes('formatSummary.summaryOnlyField'))).toBe(true); + }); + + // THE GUARANTEE that reconciles this with R2-1b. `sharedWithDeclared` is + // both a named-object key and a return-shape key; a read must still resolve + // to the DECLARED one, or indexing return shapes would silently move + // answers that already worked. + it('never outranks a declared anchor', () => { + expect(readersOf('sharedWithDeclared')).toContain('readsShared'); + const declaredWins = getRelationships(result, 'ACCESSES').filter( + (e) => e.target === 'sharedWithDeclared' && e.source === 'readsShared', + ); + expect(declaredWins.length).toBeGreaterThan(0); + }); + }); + // R2. Strict workspace uniqueness was measurably too blunt: in the reporting // repo `exitMinAtrMult` had 26 definitions, 16 of them in one-off scripts the // backend has no relationship with, so every backend read was refused because diff --git a/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts b/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts index 52bcbc9df..1cab69f7d 100644 --- a/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts +++ b/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts @@ -117,7 +117,10 @@ describe('TypeScript type-alias and interface members (A4)', () => { }); it('does not mint a member for an inline RETURN type', () => { - expect(propertyIds().some((id) => id.includes('inlineReturnOnlyKey'))).toBe(false); + // The TYPE's member, not the returned value's key — those are different + // rules with opposite expectations, and the fixture names them apart so + // this assertion cannot be satisfied by the wrong one. + expect(propertyIds().some((id) => id.includes('inlineReturnTypeOnlyKey'))).toBe(false); }); // The other half: anchoring must not cost real members. diff --git a/gitnexus/test/unit/incremental-parse-cache.test.ts b/gitnexus/test/unit/incremental-parse-cache.test.ts index c8c22b5e4..192360e9d 100644 --- a/gitnexus/test/unit/incremental-parse-cache.test.ts +++ b/gitnexus/test/unit/incremental-parse-cache.test.ts @@ -141,8 +141,8 @@ describe('PARSE_CACHE_VERSION', () => { // a row. Same lesson as the note above — the pin cannot detect the tie, since // both sides asserted `toBe(45)` and that passes while main is already 45. // Only the merge-time diff against origin/main surfaces it. - it('pins SCHEMA_BUMP to 48 so concurrent bumps cannot silently collide (#2766)', () => { - expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(48); + it('pins SCHEMA_BUMP to 49 so concurrent bumps cannot silently collide (#2766)', () => { + expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(49); }); it('embeds the gitnexus package version (so upgrades invalidate the cache)', () => {