mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
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:
parent
c6a291de67
commit
eece6344fc
2 changed files with 235 additions and 0 deletions
83
gitnexus/src/core/ingestion/registry-primary-flag.ts
Normal file
83
gitnexus/src/core/ingestion/registry-primary-flag.ts
Normal 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());
|
||||
}
|
||||
152
gitnexus/test/unit/registry-primary-flag.test.ts
Normal file
152
gitnexus/test/unit/registry-primary-flag.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue