Update CBS Mobile App codebase
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Reports feature keys that nothing in the app actually gates.
|
||||
*
|
||||
* Why this exists: a key can be declared, configured, published by the server
|
||||
* and shown in the toggle screens while no UI consults it. Switching it off then
|
||||
* appears to work and changes nothing — the worst possible failure for a kill
|
||||
* switch, because it is silent and is only discovered when someone needs it.
|
||||
*
|
||||
* A key counts as gated when it is referenced anywhere outside its own
|
||||
* declaration (`featureKeys.ts`) and the static config (`featureConfig.ts`).
|
||||
* Parent keys need no direct reference: the service resolves ancestors, so a key
|
||||
* with a gated descendant is effective through the hierarchy and is reported
|
||||
* separately rather than as a fault.
|
||||
*
|
||||
* node scripts/audit-features.js # report
|
||||
* node scripts/audit-features.js --strict # exit 1 if any key is unwired
|
||||
*
|
||||
* `--strict` is what CI should run once the list below is empty.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const SRC = path.join(ROOT, 'src');
|
||||
const KEYS_FILE = path.join(SRC, 'constants', 'featureKeys.ts');
|
||||
const EXCLUDED = new Set(['featureKeys.ts', 'featureConfig.ts']);
|
||||
|
||||
/**
|
||||
* Keys knowingly declared ahead of the UI that will consult them, with the
|
||||
* reason. Listed here so the audit stays useful — an unreviewed key shows up as
|
||||
* a finding, while these show as accepted.
|
||||
*
|
||||
* Remove an entry when you wire its UI up. Do not add one to quieten the audit
|
||||
* unless the feature genuinely has no screen yet.
|
||||
*/
|
||||
const ACCEPTED_UNWIRED = {
|
||||
'auth.login.forgotPassword':
|
||||
'No forgot-MPIN flow exists by policy — a reset needs a branch visit.',
|
||||
'auth.login.createAccount': 'Account opening is not in the customer app yet.',
|
||||
'home.services': 'Home services grid is commented out in HomeScreen.',
|
||||
'home.services.transfer': 'Part of the commented-out home services grid.',
|
||||
'home.services.selfTransfer': 'Part of the commented-out home services grid.',
|
||||
'home.services.billPay': 'Part of the commented-out home services grid.',
|
||||
'home.services.collectRequest': 'Part of the commented-out home services grid.',
|
||||
'home.services.managePayee': 'Part of the commented-out home services grid.',
|
||||
'home.services.support': 'Part of the commented-out home services grid.',
|
||||
'home.services.insurance': 'Part of the commented-out home services grid.',
|
||||
'home.services.deposits': 'Part of the commented-out home services grid.',
|
||||
'home.services.goldLoan': 'Part of the commented-out home services grid.',
|
||||
'home.insights': 'InsightsCard component exists but is not rendered anywhere.',
|
||||
'home.promoBanner': 'PromoBanner component exists but is not rendered anywhere.',
|
||||
'payments.upi': 'No UPI screens have been built.',
|
||||
'payments.upi.scan': 'No UPI screens have been built.',
|
||||
};
|
||||
|
||||
const walk = (dir, files = []) => {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
walk(full, files);
|
||||
} else if (/\.tsx?$/.test(entry.name) && !EXCLUDED.has(entry.name)) {
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
const source = fs.readFileSync(KEYS_FILE, 'utf8');
|
||||
const declarations = [...source.matchAll(/^\s+([A-Z_0-9]+) = '([a-zA-Z0-9.]+)',/gm)].map((m) => ({
|
||||
member: m[1],
|
||||
path: m[2],
|
||||
}));
|
||||
|
||||
if (declarations.length === 0) {
|
||||
console.error('No feature keys found — has featureKeys.ts moved or changed shape?');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const corpus = walk(SRC)
|
||||
.map((file) => fs.readFileSync(file, 'utf8'))
|
||||
.join('\n');
|
||||
|
||||
const referenced = new Set(
|
||||
[...corpus.matchAll(/FeatureKey\.([A-Z_0-9]+)/g)].map((m) => m[1]),
|
||||
);
|
||||
|
||||
const gatedPaths = new Set(
|
||||
declarations.filter((d) => referenced.has(d.member)).map((d) => d.path),
|
||||
);
|
||||
|
||||
const viaHierarchy = [];
|
||||
const unwired = [];
|
||||
const accepted = [];
|
||||
|
||||
for (const declaration of declarations) {
|
||||
if (gatedPaths.has(declaration.path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ([...gatedPaths].some((gated) => gated.startsWith(`${declaration.path}.`))) {
|
||||
viaHierarchy.push(declaration);
|
||||
} else if (ACCEPTED_UNWIRED[declaration.path]) {
|
||||
accepted.push(declaration);
|
||||
} else {
|
||||
unwired.push(declaration);
|
||||
}
|
||||
}
|
||||
|
||||
const pad = (value, width) => String(value).padEnd(width);
|
||||
|
||||
console.log(`\nFeature gating audit — ${declarations.length} declared keys\n`);
|
||||
console.log(` gated directly ${gatedPaths.size}`);
|
||||
console.log(` via hierarchy ${viaHierarchy.length} (parent of a gated key)`);
|
||||
console.log(` accepted unwired ${accepted.length} (no UI yet, reason recorded)`);
|
||||
console.log(` UNWIRED ${unwired.length}`);
|
||||
|
||||
if (accepted.length > 0) {
|
||||
console.log('\nAccepted — declared ahead of their UI:');
|
||||
for (const { path: keyPath } of accepted.sort((a, b) => a.path.localeCompare(b.path))) {
|
||||
console.log(` ${pad(keyPath, 34)} ${ACCEPTED_UNWIRED[keyPath]}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (unwired.length > 0) {
|
||||
console.log('\nUNWIRED — declared, configurable, but nothing reads them.');
|
||||
console.log('Switching one of these off will appear to work and change nothing.\n');
|
||||
for (const { path: keyPath, member } of unwired.sort((a, b) => a.path.localeCompare(b.path))) {
|
||||
console.log(` ${pad(keyPath, 40)} FeatureKey.${member}`);
|
||||
}
|
||||
console.log('\nEither gate the UI it should control, or record it in ACCEPTED_UNWIRED');
|
||||
console.log('in this script with the reason.\n');
|
||||
} else {
|
||||
console.log('\nEvery declared key is either gated or has a recorded reason.\n');
|
||||
}
|
||||
|
||||
if (process.argv.includes('--strict') && unwired.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user