feat(ingestion): REGISTRY_PRIMARY_<LANG> per-language flag reader (#924, RFC #909 Ring 2 PKG) (#968)

Adds the per-language feature flag primitive that gates the Ring 3
registry-primary rollout. Single source of truth for whether a given
language uses `Registry.lookup` (new) or the legacy DAG (current).

## Shipped

### `gitnexus/src/core/ingestion/registry-primary-flag.ts`

  - `isRegistryPrimary(lang): boolean` — reads
    `REGISTRY_PRIMARY_<UPPER(enum-value)>` from `process.env`.
  - `envVarNameFor(lang): string` — exposed for CI tooling that
    cross-references flag flips (and for test assertions).
  - `primaryLanguages(): ReadonlySet<SupportedLanguages>` — all
    currently-on languages; useful for startup logging + the #923
    shadow dashboard which distinguishes "primary: legacy" vs
    "primary: registry" rows.

### Contract

  - Default: `false` for every language. A language must explicitly
    opt in by setting its env var.
  - Truthy: `'true'`, `'1'`, `'yes'` (case-insensitive, whitespace-
    trimmed). Anything else — typos, empty string, `'off'` — is
    `false`. Fail-safe posture: a misspelled flag doesn't accidentally
    flip a language.
  - No per-process caching. `process.env` is read per call; overhead
    is negligible (one lookup per file at resolution time), and
    test isolation is lexical (no cache-reset coordination).

### Env-var mapping

Uses the enum VALUE, not the TS key, for the env-var suffix:

  - `SupportedLanguages.Python`     → `REGISTRY_PRIMARY_PYTHON`
  - `SupportedLanguages.CPlusPlus`  → `REGISTRY_PRIMARY_CPP`   (value `'cpp'`)
  - `SupportedLanguages.CSharp`     → `REGISTRY_PRIMARY_CSHARP`

Users flip languages by their canonical name, not the TS symbol.

## Tests (16, all passing)

  - `envVarNameFor` (3): upper-casing · enum-VALUE-not-KEY mapping ·
    all-languages uniqueness smoke-test
  - `isRegistryPrimary` (9): default false · `'true'` / `'1'` / `'yes'`
    truthy · mixed-case + whitespace-padded · falsy-looking values ·
    unrecognized tokens (typo-safe) · per-language isolation · no
    stale cache on mid-process mutation · CPlusPlus mapping
  - `primaryLanguages` (3): empty · exact membership · Set instanceof

Tests scrub every `REGISTRY_PRIMARY_*` env var in `beforeEach` +
`afterEach` so parallel vitest runs on the same process don't bleed state.

## What's NOT in this PR (deferred by design)

The actual integration in `call-processor.ts` belongs in #921
(finalize-orchestrator). Reason: the "new path" requires a populated
`SemanticModel` to call `Registry.lookup` against, and the model
becomes accessible only after #921 orchestrates finalize. Wiring a
dead branch now would just get rewritten then.

This PR ships the flag primitive in isolation so #921 has a clean,
tested utility to consult — and so `#923` (shadow harness) has a
stable boolean to read for its "which row is primary?" rendering.

## Closes part of #909. Unblocks
  - #921 finalize-orchestrator — can now consult `isRegistryPrimary`
    at resolution time
  - #923 shadow harness — can distinguish primary-flipped rows
This commit is contained in:
Gergő Magyar 2026-04-18 19:54:54 +01:00 • committed by GitHub
parent c6a291de67
commit eece6344fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 235 additions and 0 deletions

View file

@ -0,0 +1,83 @@
/**
* `REGISTRY_PRIMARY_<LANG>` per-language feature flags for the scope-based
* resolution rollout (RFC §6.1 Ring 3; Ring 2 PKG #924).
*
* This module is the single source of truth for whether a given language
* has been flipped to registry-primary call resolution. When a language's
* flag is true, its files route through `Registry.lookup` (RFC §4) instead
* of the legacy call-resolution DAG; when false (the default), the legacy
* DAG runs unchanged.
*
* ## Contract
*
* - Env-var name per language: `REGISTRY_PRIMARY_<UPPER(enum-value)>`.
* Example: `SupportedLanguages.Python` → `REGISTRY_PRIMARY_PYTHON`;
* `SupportedLanguages.CPlusPlus` (value `'cpp'`) → `REGISTRY_PRIMARY_CPP`.
* - Truthy values: `'true'`, `'1'`, `'yes'` (case-insensitive,
* whitespace-trimmed). Anything else — including `undefined`, empty
* string, or unknown tokens — is `false`.
* - No per-process caching. `process.env` is read on every call. The
* flag is consulted once per file at call-resolution time, so the
* overhead is negligible; skipping caching keeps test isolation
* trivial (no `resetFlagCache()` coordination needed).
*
* ## Integration site
*
* `call-processor.ts` integration lands in **#921** (`finalize-orchestrator`)
* where the `SemanticModel` becomes accessible and `Registry.lookup` can
* actually be called with a populated context. This module ships the flag
* primitive in isolation so #921 has a clean, tested utility to consult.
*
* ## Shadow mode is orthogonal
*
* Shadow mode (`GITNEXUS_SHADOW_MODE=1`, introduced in #923) runs BOTH
* legacy and registry paths regardless of the per-language flag, so the
* parity dashboard has signal even for un-flipped languages. That logic
* lives in `shadow-harness.ts` (#923), not here.
*/
import { SupportedLanguages } from 'gitnexus-shared';
/**
* Return the env-var name that controls a given language's registry-
* primary flag. Exported for test assertions and for the PR-labeling
* CI job that cross-references per-language flag changes.
*/
export function envVarNameFor(lang: SupportedLanguages): string {
return `REGISTRY_PRIMARY_${lang.toUpperCase()}`;
}
/**
* Whether `lang` has been flipped to registry-primary call resolution.
*
* Returns `false` by default — a language must explicitly set its env
* var to a truthy value to opt in. The flag is the sole control surface:
* flipping it requires no code change, and reverting it requires no code
* change.
*/
export function isRegistryPrimary(lang: SupportedLanguages): boolean {
return parseFlag(process.env[envVarNameFor(lang)]);
}
/**
* All languages whose registry-primary flag is currently on. Useful for
* startup-time logging + the shadow-harness dashboard, which wants to
* distinguish "primary: legacy" from "primary: registry" rows.
*/
export function primaryLanguages(): ReadonlySet<SupportedLanguages> {
const out = new Set<SupportedLanguages>();
for (const lang of Object.values(SupportedLanguages)) {
if (isRegistryPrimary(lang)) out.add(lang);
}
return out;
}
// ─── Internal ───────────────────────────────────────────────────────────────
/** Accepted truthy strings (case-insensitive, trimmed). */
const TRUTHY_VALUES: ReadonlySet<string> = new Set(['true', '1', 'yes']);
function parseFlag(raw: string | undefined): boolean {
if (raw === undefined) return false;
return TRUTHY_VALUES.has(raw.trim().toLowerCase());
}

View file

@ -0,0 +1,152 @@
/**
* Unit tests for `registry-primary-flag` (RFC #909 Ring 2 PKG #924).
*
* Flag is `REGISTRY_PRIMARY_<UPPER(lang)>`. Each test manipulates
* `process.env` directly and restores it in `afterEach` — there is no
* per-process cache to invalidate, so isolation is lexical.
*/
import { describe, it, expect, afterEach, beforeEach } from 'vitest';
import { SupportedLanguages } from 'gitnexus-shared';
import {
envVarNameFor,
isRegistryPrimary,
primaryLanguages,
} from '../../src/core/ingestion/registry-primary-flag.js';
// ─── Test isolation ─────────────────────────────────────────────────────────
//
// Scrub every `REGISTRY_PRIMARY_*` env var before + after each test so
// parallel vitest runs on the same process don't bleed state.
function clearAllRegistryPrimaryVars(): void {
for (const key of Object.keys(process.env)) {
if (key.startsWith('REGISTRY_PRIMARY_')) delete process.env[key];
}
}
beforeEach(clearAllRegistryPrimaryVars);
afterEach(clearAllRegistryPrimaryVars);
// ─── envVarNameFor ─────────────────────────────────────────────────────────
describe('envVarNameFor', () => {
it('produces upper-cased env-var names from the enum value', () => {
expect(envVarNameFor(SupportedLanguages.Python)).toBe('REGISTRY_PRIMARY_PYTHON');
expect(envVarNameFor(SupportedLanguages.TypeScript)).toBe('REGISTRY_PRIMARY_TYPESCRIPT');
expect(envVarNameFor(SupportedLanguages.JavaScript)).toBe('REGISTRY_PRIMARY_JAVASCRIPT');
});
it('uses the enum VALUE, not the key, for languages whose key differs from the value', () => {
// Key 'CPlusPlus' → value 'cpp' → env var 'REGISTRY_PRIMARY_CPP'.
// Users see the language by its canonical name, not its TS symbol.
expect(envVarNameFor(SupportedLanguages.CPlusPlus)).toBe('REGISTRY_PRIMARY_CPP');
expect(envVarNameFor(SupportedLanguages.CSharp)).toBe('REGISTRY_PRIMARY_CSHARP');
});
it('covers every member of SupportedLanguages', () => {
// Build env-var names for every language and assert no duplicates —
// catches a future enum-value collision or accidental renaming.
const names = new Set<string>();
for (const lang of Object.values(SupportedLanguages)) {
names.add(envVarNameFor(lang));
}
expect(names.size).toBe(Object.values(SupportedLanguages).length);
});
});
// ─── isRegistryPrimary ─────────────────────────────────────────────────────
describe('isRegistryPrimary', () => {
it('returns false by default (no env var set)', () => {
for (const lang of Object.values(SupportedLanguages)) {
expect(isRegistryPrimary(lang)).toBe(false);
}
});
it("returns true when the env var is 'true' (lowercase)", () => {
process.env['REGISTRY_PRIMARY_PYTHON'] = 'true';
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
});
it("returns true when the env var is '1'", () => {
process.env['REGISTRY_PRIMARY_PYTHON'] = '1';
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
});
it("returns true when the env var is 'yes'", () => {
process.env['REGISTRY_PRIMARY_PYTHON'] = 'yes';
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
});
it('accepts mixed-case and whitespace-padded truthy values', () => {
process.env['REGISTRY_PRIMARY_PYTHON'] = ' TRUE ';
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
process.env['REGISTRY_PRIMARY_PYTHON'] = 'Yes';
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
});
it("returns false for falsy-looking values ('false', '0', empty, 'off')", () => {
for (const value of ['false', '0', '', 'off', 'no', 'disabled']) {
process.env['REGISTRY_PRIMARY_PYTHON'] = value;
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(false);
}
});
it('returns false for unrecognized tokens (fail-safe on typos)', () => {
// User meant to type 'true' but fat-fingered — conservative: treat as off.
for (const value of ['ture', 'tru', 'yeah', 'enable', 'y']) {
process.env['REGISTRY_PRIMARY_PYTHON'] = value;
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(false);
}
});
it('isolates flags per-language (one on does not affect others)', () => {
process.env['REGISTRY_PRIMARY_PYTHON'] = 'true';
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
expect(isRegistryPrimary(SupportedLanguages.Java)).toBe(false);
expect(isRegistryPrimary(SupportedLanguages.Go)).toBe(false);
});
it('respects a mid-process env-var mutation (no stale cache)', () => {
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(false);
process.env['REGISTRY_PRIMARY_PYTHON'] = 'true';
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
delete process.env['REGISTRY_PRIMARY_PYTHON'];
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(false);
});
it('handles the CPlusPlus → REGISTRY_PRIMARY_CPP mapping correctly', () => {
process.env['REGISTRY_PRIMARY_CPP'] = 'true';
expect(isRegistryPrimary(SupportedLanguages.CPlusPlus)).toBe(true);
// Negative: the TS-key-style name is NOT read.
delete process.env['REGISTRY_PRIMARY_CPP'];
process.env['REGISTRY_PRIMARY_CPLUSPLUS'] = 'true';
expect(isRegistryPrimary(SupportedLanguages.CPlusPlus)).toBe(false);
});
});
// ─── primaryLanguages ──────────────────────────────────────────────────────
describe('primaryLanguages', () => {
it('returns an empty set when no flags are set', () => {
expect(primaryLanguages().size).toBe(0);
});
it('returns exactly the flipped languages', () => {
process.env['REGISTRY_PRIMARY_PYTHON'] = 'true';
process.env['REGISTRY_PRIMARY_GO'] = '1';
process.env['REGISTRY_PRIMARY_JAVA'] = 'false'; // explicitly off
const enabled = primaryLanguages();
expect(enabled.has(SupportedLanguages.Python)).toBe(true);
expect(enabled.has(SupportedLanguages.Go)).toBe(true);
expect(enabled.has(SupportedLanguages.Java)).toBe(false);
expect(enabled.size).toBe(2);
});
it('returns a plain Set (not a frozen proxy) — consistent shape', () => {
process.env['REGISTRY_PRIMARY_PYTHON'] = 'true';
const enabled = primaryLanguages();
expect(enabled).toBeInstanceOf(Set);
});
});