pumuki-ast-hooks 5.3.18 → 5.3.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/docs/VIOLATIONS_RESOLUTION_PLAN.md +23 -25
  2. package/package.json +2 -2
  3. package/scripts/hooks-system/application/CompositionRoot.js +24 -73
  4. package/scripts/hooks-system/application/services/DynamicRulesLoader.js +1 -2
  5. package/scripts/hooks-system/application/services/GitTreeState.js +139 -13
  6. package/scripts/hooks-system/application/services/HookSystemScheduler.js +43 -0
  7. package/scripts/hooks-system/application/services/PlaybookRunner.js +1 -1
  8. package/scripts/hooks-system/application/services/RealtimeGuardService.js +15 -85
  9. package/scripts/hooks-system/application/services/guard/GuardAutoManagerService.js +2 -31
  10. package/scripts/hooks-system/application/services/guard/GuardConfig.js +9 -17
  11. package/scripts/hooks-system/application/services/guard/GuardHeartbeatMonitor.js +9 -6
  12. package/scripts/hooks-system/application/services/guard/GuardProcessManager.js +0 -29
  13. package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +1 -4
  14. package/scripts/hooks-system/application/services/installation/McpConfigurator.js +1 -2
  15. package/scripts/hooks-system/application/services/logging/AuditLogger.js +86 -1
  16. package/scripts/hooks-system/application/services/logging/UnifiedLogger.js +4 -13
  17. package/scripts/hooks-system/application/services/monitoring/EvidenceMonitor.js +1 -0
  18. package/scripts/hooks-system/application/services/monitoring/EvidenceMonitorService.js +3 -7
  19. package/scripts/hooks-system/application/services/token/TokenMetricsService.js +1 -14
  20. package/scripts/hooks-system/bin/__tests__/evidence-update.spec.js +49 -0
  21. package/scripts/hooks-system/bin/cli.js +1 -15
  22. package/scripts/hooks-system/domain/events/index.js +6 -32
  23. package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidAnalysisOrchestrator.js +2 -3
  24. package/scripts/hooks-system/infrastructure/ast/ast-core.js +20 -12
  25. package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +18 -8
  26. package/scripts/hooks-system/infrastructure/ast/backend/analyzers/BackendPatternDetector.js +1 -2
  27. package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +8 -10
  28. package/scripts/hooks-system/infrastructure/ast/frontend/ast-frontend.js +196 -196
  29. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/__tests__/iOSASTIntelligentAnalyzer.spec.js +66 -0
  30. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +2 -3
  31. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSArchitectureRules.js +24 -86
  32. package/scripts/hooks-system/infrastructure/hooks/skill-activation-prompt.js +2 -3
  33. package/scripts/hooks-system/infrastructure/logging/UnifiedLoggerFactory.js +5 -35
  34. package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +16 -86
  35. package/scripts/hooks-system/infrastructure/telemetry/metrics-server.js +2 -51
  36. package/scripts/hooks-system/infrastructure/validators/enforce-english-literals.js +8 -6
@@ -57,8 +57,8 @@ function runFrontendIntelligence(project, findings, platform) {
57
57
  return 0.6745 * (x - med) / madValue;
58
58
  };
59
59
 
60
- const pOutlier = env.getNumber('AST_GODCLASS_P_OUTLIER', 90);
61
- const pExtreme = env.getNumber('AST_GODCLASS_P_EXTREME', 97);
60
+ const pOutlier = Number(process.env.AST_GODCLASS_P_OUTLIER || 90);
61
+ const pExtreme = Number(process.env.AST_GODCLASS_P_EXTREME || 97);
62
62
 
63
63
  const componentPattern = /^(export\s+)?(?:const|function)\s+([A-Z]\w+)\s*[=:].*(?:React\.FC|JSX\.Element|\(\)\s*=>|function)/gm;
64
64
  const metrics = [];
@@ -1129,10 +1129,10 @@ function runFrontendIntelligence(project, findings, platform) {
1129
1129
  }
1130
1130
  if (/page\.tsx$/.test(filePath)) {
1131
1131
  const candidate = filePath.replace(/\/app\//, '/e2e/').replace(/\.tsx$/, '.spec.ts');
1132
- try {
1133
- if (!fs.existsSync(candidate)) {
1134
- pushFinding("frontend.testing.missing_e2e", "info", sf, sf, "No E2E spec found for this page (heuristic)", findings);
1135
- }
1132
+ try {
1133
+ if (!fs.existsSync(candidate)) {
1134
+ pushFinding("frontend.testing.missing_e2e", "info", sf, sf, "No E2E spec found for this page (heuristic)", findings);
1135
+ }
1136
1136
  } catch (error) {
1137
1137
  if (process.env.DEBUG) {
1138
1138
  console.debug(`[frontend-ast] Failed to check E2E spec file: ${error.message}`);
@@ -1181,14 +1181,14 @@ function runFrontendIntelligence(project, findings, platform) {
1181
1181
  const extreme = totalComplexityZ >= godComponentBaseline.thresholds.extreme.totalComplexityZ || bodyLinesZ >= godComponentBaseline.thresholds.extreme.bodyLinesZ;
1182
1182
 
1183
1183
  if (extreme || (complexityOutlier && sizeOutlier)) {
1184
- pushFinding(
1185
- "frontend.solid.srp_god_component",
1186
- "critical",
1187
- sf,
1188
- sf,
1189
- `Component '${componentName}' has ${hookCount} hooks + ${functionCount} functions = ${totalComplexity} (z=${totalComplexityZ.toFixed(2)}), ${bodyLines} lines (z=${bodyLinesZ.toFixed(2)}) - split responsibilities (SRP)`,
1190
- findings
1191
- );
1184
+ pushFinding(
1185
+ "frontend.solid.srp_god_component",
1186
+ "critical",
1187
+ sf,
1188
+ sf,
1189
+ `Component '${componentName}' has ${hookCount} hooks + ${functionCount} functions = ${totalComplexity} (z=${totalComplexityZ.toFixed(2)}), ${bodyLines} lines (z=${bodyLinesZ.toFixed(2)}) - split responsibilities (SRP)`,
1190
+ findings
1191
+ );
1192
1192
  }
1193
1193
  }
1194
1194
  });
@@ -1823,269 +1823,269 @@ function runFrontendIntelligence(project, findings, platform) {
1823
1823
  }
1824
1824
 
1825
1825
  const useStateCallsInComponent = sf.getDescendantsOfKind(SyntaxKind.CallExpression)
1826
- .filter(call => call.getExpression().getText() === 'useState');
1827
-
1828
- sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).forEach(fn => {
1829
- const name = fn.getName();
1830
- if (name && /^[A-Z]/.test(name)) {
1831
- const useStatesInFn = useStateCallsInComponent.filter(call =>
1832
- fn.getDescendants().includes(call)
1833
- );
1834
-
1835
- if (useStatesInFn.length >= 4) {
1836
- pushFinding(
1837
- "frontend.hooks.useState_overuse",
1838
- "medium",
1839
- sf,
1840
- fn,
1841
- `Component ${name} has ${useStatesInFn.length}+ useState. Consider useReducer for complex state. Benefits: Single state object, predictable updates, easier testing.`,
1842
- findings
1843
- );
1844
- }
1845
- }
1846
- });
1847
-
1848
- const contextCreations = sf.getDescendantsOfKind(SyntaxKind.CallExpression)
1849
- .filter(call => call.getExpression().getText() === 'createContext');
1826
+ .filter(call => call.getExpression().getText() === 'useState');
1850
1827
 
1851
- if (contextCreations.length >= 3 && !sf.getFilePath().includes('context')) {
1852
- pushFinding(
1853
- "frontend.state.context_overuse",
1854
- "medium",
1855
- sf,
1856
- sf,
1857
- `Multiple Context creations (${contextCreations.length}). Consider Zustand for global state. Context rerenders all consumers. Zustand: Selective subscriptions, better performance.`,
1858
- findings
1828
+ sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).forEach(fn => {
1829
+ const name = fn.getName();
1830
+ if (name && /^[A-Z]/.test(name)) {
1831
+ const useStatesInFn = useStateCallsInComponent.filter(call =>
1832
+ fn.getDescendants().includes(call)
1859
1833
  );
1860
- }
1861
-
1862
- sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).forEach(fn => {
1863
- const name = fn.getName();
1864
- const body = fn.getBody()?.getText() || '';
1865
- const hasHookCall = /use[A-Z]/.test(body);
1866
1834
 
1867
- if (name && hasHookCall && !/^use[A-Z]/.test(name)) {
1835
+ if (useStatesInFn.length >= 4) {
1868
1836
  pushFinding(
1869
- "frontend.hooks.naming_convention",
1837
+ "frontend.hooks.useState_overuse",
1870
1838
  "medium",
1871
1839
  sf,
1872
1840
  fn,
1873
- `Function ${name} uses hooks but doesn't start with 'use'. Rename to use${name[0].toUpperCase()}${name.slice(1)}. React ESLint requires 'use' prefix for hook functions.`,
1841
+ `Component ${name} has ${useStatesInFn.length}+ useState. Consider useReducer for complex state. Benefits: Single state object, predictable updates, easier testing.`,
1874
1842
  findings
1875
1843
  );
1876
1844
  }
1877
- });
1845
+ }
1846
+ });
1847
+
1848
+ const contextCreations = sf.getDescendantsOfKind(SyntaxKind.CallExpression)
1849
+ .filter(call => call.getExpression().getText() === 'createContext');
1850
+
1851
+ if (contextCreations.length >= 3 && !sf.getFilePath().includes('context')) {
1852
+ pushFinding(
1853
+ "frontend.state.context_overuse",
1854
+ "medium",
1855
+ sf,
1856
+ sf,
1857
+ `Multiple Context creations (${contextCreations.length}). Consider Zustand for global state. Context rerenders all consumers. Zustand: Selective subscriptions, better performance.`,
1858
+ findings
1859
+ );
1860
+ }
1878
1861
 
1879
- const fullText = sf.getFullText();
1880
- const hasLargeImports = fullText.includes('import * as') ||
1881
- fullText.match(/import\s+\{[^}]{200,}\}/);
1862
+ sf.getDescendantsOfKind(SyntaxKind.FunctionDeclaration).forEach(fn => {
1863
+ const name = fn.getName();
1864
+ const body = fn.getBody()?.getText() || '';
1865
+ const hasHookCall = /use[A-Z]/.test(body);
1882
1866
 
1883
- if (hasLargeImports) {
1867
+ if (name && hasHookCall && !/^use[A-Z]/.test(name)) {
1884
1868
  pushFinding(
1885
- "frontend.performance.large_imports",
1869
+ "frontend.hooks.naming_convention",
1886
1870
  "medium",
1887
1871
  sf,
1888
- sf,
1889
- 'Large import detected. Use tree-shaking: import { Button } from \'@mui/material/Button\' (not from \'@mui/material\'). Reduces bundle size by importing only needed components.',
1872
+ fn,
1873
+ `Function ${name} uses hooks but doesn't start with 'use'. Rename to use${name[0].toUpperCase()}${name.slice(1)}. React ESLint requires 'use' prefix for hook functions.`,
1890
1874
  findings
1891
1875
  );
1892
1876
  }
1877
+ });
1893
1878
 
1894
- if (content.includes('class') && content.includes('extends Component') &&
1895
- !content.includes('componentDidCatch') && !content.includes('getDerivedStateFromError')) {
1879
+ const fullText = sf.getFullText();
1880
+ const hasLargeImports = fullText.includes('import * as') ||
1881
+ fullText.match(/import\s+\{[^}]{200,}\}/);
1882
+
1883
+ if (hasLargeImports) {
1884
+ pushFinding(
1885
+ "frontend.performance.large_imports",
1886
+ "medium",
1887
+ sf,
1888
+ sf,
1889
+ 'Large import detected. Use tree-shaking: import { Button } from \'@mui/material/Button\' (not from \'@mui/material\'). Reduces bundle size by importing only needed components.',
1890
+ findings
1891
+ );
1892
+ }
1896
1893
 
1897
- const hasChildrenRender = content.includes('render()') && content.includes('children');
1894
+ if (content.includes('class') && content.includes('extends Component') &&
1895
+ !content.includes('componentDidCatch') && !content.includes('getDerivedStateFromError')) {
1898
1896
 
1899
- if (hasChildrenRender) {
1900
- pushFinding(
1901
- "frontend.error_handling.missing_error_boundary",
1902
- "medium",
1903
- sf,
1904
- sf,
1905
- 'Component renders children without error boundary. Add: componentDidCatch(error, errorInfo) { logError(error); }. Prevents whole app crash from single component error.',
1906
- findings
1907
- );
1908
- }
1909
- }
1910
-
1911
- const hasRoutes = content.includes('Route') || content.includes('router');
1912
- const hasLazy = content.includes('React.lazy') || content.includes('dynamic(');
1897
+ const hasChildrenRender = content.includes('render()') && content.includes('children');
1913
1898
 
1914
- if (hasRoutes && !hasLazy && sf.getFilePath().includes('app')) {
1899
+ if (hasChildrenRender) {
1915
1900
  pushFinding(
1916
- "frontend.performance.missing_lazy_loading",
1901
+ "frontend.error_handling.missing_error_boundary",
1917
1902
  "medium",
1918
1903
  sf,
1919
1904
  sf,
1920
- 'Routes without lazy loading. Use: const Dashboard = lazy(() => import(\'./Dashboard\')). Wrap with <Suspense>. Reduces initial bundle, faster First Contentful Paint.',
1905
+ 'Component renders children without error boundary. Add: componentDidCatch(error, errorInfo) { logError(error); }. Prevents whole app crash from single component error.',
1921
1906
  findings
1922
1907
  );
1923
1908
  }
1909
+ }
1924
1910
 
1911
+ const hasRoutes = content.includes('Route') || content.includes('router');
1912
+ const hasLazy = content.includes('React.lazy') || content.includes('dynamic(');
1913
+
1914
+ if (hasRoutes && !hasLazy && sf.getFilePath().includes('app')) {
1915
+ pushFinding(
1916
+ "frontend.performance.missing_lazy_loading",
1917
+ "medium",
1918
+ sf,
1919
+ sf,
1920
+ 'Routes without lazy loading. Use: const Dashboard = lazy(() => import(\'./Dashboard\')). Wrap with <Suspense>. Reduces initial bundle, faster First Contentful Paint.',
1921
+ findings
1922
+ );
1923
+ }
1925
1924
 
1926
- if (sf.getFilePath().includes('layout.tsx') || sf.getFilePath().includes('page.tsx')) {
1927
- const hasMetadata = content.includes('metadata') || content.includes('generateMetadata');
1928
-
1929
- if (!hasMetadata) {
1930
- pushFinding(
1931
- "frontend.seo.missing_metadata",
1932
- "low",
1933
- sf,
1934
- sf,
1935
- 'Missing SEO metadata. Export: export const metadata = { title, description, openGraph }. Improves Google ranking, social media previews.',
1936
- findings
1937
- );
1938
- }
1939
- }
1940
-
1941
- if (sf.getFilePath().includes('sitemap')) {
1942
- const hasDynamicRoutes = content.includes('fetch') || content.includes('database');
1943
-
1944
- if (!hasDynamicRoutes && content.length < 200) {
1945
- pushFinding(
1946
- "frontend.seo.static_sitemap",
1947
- "low",
1948
- sf,
1949
- sf,
1950
- 'Static sitemap. Generate dynamically: fetch all routes from API/DB. Update automatically when content changes. Better SEO indexing.',
1951
- findings
1952
- );
1953
- }
1954
- }
1955
-
1956
- if (sf.getFilePath().includes('robots')) {
1957
- const hasDisallow = content.includes('Disallow');
1958
-
1959
- if (!hasDisallow) {
1960
- pushFinding(
1961
- "frontend.seo.robots_config",
1962
- "low",
1963
- sf,
1964
- sf,
1965
- 'Robots.txt incomplete. Add Disallow rules for: /api, /admin, /_next. Prevents search engines indexing private routes.',
1966
- findings
1967
- );
1968
- }
1969
- }
1970
-
1971
- if (sf.getFilePath().includes('manifest')) {
1972
- const hasIcons = content.includes('icons');
1973
- const hasThemeColor = content.includes('theme_color');
1974
1925
 
1975
- if (!hasIcons || !hasThemeColor) {
1976
- pushFinding(
1977
- "frontend.pwa.manifest_incomplete",
1978
- "low",
1979
- sf,
1980
- sf,
1981
- 'PWA manifest incomplete. Add: icons (192x192, 512x512), theme_color, background_color, display: standalone. Enables Add to Home Screen.',
1982
- findings
1983
- );
1984
- }
1985
- }
1926
+ if (sf.getFilePath().includes('layout.tsx') || sf.getFilePath().includes('page.tsx')) {
1927
+ const hasMetadata = content.includes('metadata') || content.includes('generateMetadata');
1986
1928
 
1987
- if (content.includes('serviceWorker') && !content.includes('unregister')) {
1929
+ if (!hasMetadata) {
1988
1930
  pushFinding(
1989
- "frontend.pwa.missing_sw_unregister",
1931
+ "frontend.seo.missing_metadata",
1990
1932
  "low",
1991
1933
  sf,
1992
1934
  sf,
1993
- 'Service Worker registration without unregister fallback. Add: if (!production) navigator.serviceWorker.unregister(). Prevents caching issues in development.',
1935
+ 'Missing SEO metadata. Export: export const metadata = { title, description, openGraph }. Improves Google ranking, social media previews.',
1994
1936
  findings
1995
1937
  );
1996
1938
  }
1939
+ }
1997
1940
 
1998
- if (sf.getFilePath().includes('lighthouse') || sf.getFilePath().includes('performance')) {
1999
- const hasThresholds = content.includes('performance') && content.includes('accessibility');
1941
+ if (sf.getFilePath().includes('sitemap')) {
1942
+ const hasDynamicRoutes = content.includes('fetch') || content.includes('database');
2000
1943
 
2001
- if (!hasThresholds) {
2002
- pushFinding(
2003
- "frontend.performance.lighthouse_monitoring",
2004
- "low",
2005
- sf,
2006
- sf,
2007
- 'Lighthouse config without thresholds. Set CI thresholds: performance: 90, accessibility: 95, best-practices: 90, seo: 90. Prevents performance regressions.',
2008
- findings
2009
- );
2010
- }
1944
+ if (!hasDynamicRoutes && content.length < 200) {
1945
+ pushFinding(
1946
+ "frontend.seo.static_sitemap",
1947
+ "low",
1948
+ sf,
1949
+ sf,
1950
+ 'Static sitemap. Generate dynamically: fetch all routes from API/DB. Update automatically when content changes. Better SEO indexing.',
1951
+ findings
1952
+ );
2011
1953
  }
1954
+ }
2012
1955
 
2013
- const hasWebVitals = content.includes('reportWebVitals') || content.includes('web-vitals');
2014
- const hasAnalytics = content.includes('analytics') || content.includes('gtag');
1956
+ if (sf.getFilePath().includes('robots')) {
1957
+ const hasDisallow = content.includes('Disallow');
2015
1958
 
2016
- if (hasWebVitals && !hasAnalytics) {
1959
+ if (!hasDisallow) {
2017
1960
  pushFinding(
2018
- "frontend.monitoring.web_vitals_no_analytics",
1961
+ "frontend.seo.robots_config",
2019
1962
  "low",
2020
1963
  sf,
2021
1964
  sf,
2022
- 'Web Vitals captured but not sent to analytics. Send to Google Analytics: gtag(\'event\', metric.name, { value: metric.value }). Track LCP, FID, CLS in production.',
1965
+ 'Robots.txt incomplete. Add Disallow rules for: /api, /admin, /_next. Prevents search engines indexing private routes.',
2023
1966
  findings
2024
1967
  );
2025
1968
  }
1969
+ }
1970
+
1971
+ if (sf.getFilePath().includes('manifest')) {
1972
+ const hasIcons = content.includes('icons');
1973
+ const hasThemeColor = content.includes('theme_color');
2026
1974
 
2027
- if (sf.getFilePath().includes('layout') && !content.includes('analytics') && !content.includes('gtag')) {
1975
+ if (!hasIcons || !hasThemeColor) {
2028
1976
  pushFinding(
2029
- "frontend.monitoring.missing_analytics",
1977
+ "frontend.pwa.manifest_incomplete",
2030
1978
  "low",
2031
1979
  sf,
2032
1980
  sf,
2033
- 'Root layout without analytics. Add Google Analytics: <Script src="https://www.googletagmanager.com/gtag/js" />. Track user behavior, conversion rates.',
1981
+ 'PWA manifest incomplete. Add: icons (192x192, 512x512), theme_color, background_color, display: standalone. Enables Add to Home Screen.',
2034
1982
  findings
2035
1983
  );
2036
1984
  }
1985
+ }
1986
+
1987
+ if (content.includes('serviceWorker') && !content.includes('unregister')) {
1988
+ pushFinding(
1989
+ "frontend.pwa.missing_sw_unregister",
1990
+ "low",
1991
+ sf,
1992
+ sf,
1993
+ 'Service Worker registration without unregister fallback. Add: if (!production) navigator.serviceWorker.unregister(). Prevents caching issues in development.',
1994
+ findings
1995
+ );
1996
+ }
1997
+
1998
+ if (sf.getFilePath().includes('lighthouse') || sf.getFilePath().includes('performance')) {
1999
+ const hasThresholds = content.includes('performance') && content.includes('accessibility');
2037
2000
 
2038
- if (sf.getFilePath().includes('error.tsx') && !content.includes('Sentry') && !content.includes('captureException')) {
2001
+ if (!hasThresholds) {
2039
2002
  pushFinding(
2040
- "frontend.monitoring.missing_error_tracking",
2003
+ "frontend.performance.lighthouse_monitoring",
2041
2004
  "low",
2042
2005
  sf,
2043
2006
  sf,
2044
- 'Error page without error tracking. Install: npm i @sentry/nextjs. Track production errors: Sentry.captureException(error). Get alerts when users hit errors.',
2007
+ 'Lighthouse config without thresholds. Set CI thresholds: performance: 90, accessibility: 95, best-practices: 90, seo: 90. Prevents performance regressions.',
2045
2008
  findings
2046
2009
  );
2047
2010
  }
2011
+ }
2048
2012
 
2049
- if (content.includes('if (') && /feature|experiment|rollout|beta/i.test(content)) {
2050
- const hasFeatureFlagLib = content.includes('unleash') || content.includes('launchdarkly') || content.includes('flagsmith');
2013
+ const hasWebVitals = content.includes('reportWebVitals') || content.includes('web-vitals');
2014
+ const hasAnalytics = content.includes('analytics') || content.includes('gtag');
2015
+
2016
+ if (hasWebVitals && !hasAnalytics) {
2017
+ pushFinding(
2018
+ "frontend.monitoring.web_vitals_no_analytics",
2019
+ "low",
2020
+ sf,
2021
+ sf,
2022
+ 'Web Vitals captured but not sent to analytics. Send to Google Analytics: gtag(\'event\', metric.name, { value: metric.value }). Track LCP, FID, CLS in production.',
2023
+ findings
2024
+ );
2025
+ }
2051
2026
 
2052
- if (!hasFeatureFlagLib) {
2053
- pushFinding(
2054
- "frontend.devops.hardcoded_feature_flag",
2055
- "low",
2056
- sf,
2057
- sf,
2058
- 'Hardcoded feature flag. Use feature flag service: Unleash, LaunchDarkly, Flagsmith. Benefits: Toggle features without deployment, gradual rollout, A/B testing.',
2059
- findings
2060
- );
2061
- }
2062
- }
2027
+ if (sf.getFilePath().includes('layout') && !content.includes('analytics') && !content.includes('gtag')) {
2028
+ pushFinding(
2029
+ "frontend.monitoring.missing_analytics",
2030
+ "low",
2031
+ sf,
2032
+ sf,
2033
+ 'Root layout without analytics. Add Google Analytics: <Script src="https://www.googletagmanager.com/gtag/js" />. Track user behavior, conversion rates.',
2034
+ findings
2035
+ );
2036
+ }
2037
+
2038
+ if (sf.getFilePath().includes('error.tsx') && !content.includes('Sentry') && !content.includes('captureException')) {
2039
+ pushFinding(
2040
+ "frontend.monitoring.missing_error_tracking",
2041
+ "low",
2042
+ sf,
2043
+ sf,
2044
+ 'Error page without error tracking. Install: npm i @sentry/nextjs. Track production errors: Sentry.captureException(error). Get alerts when users hit errors.',
2045
+ findings
2046
+ );
2047
+ }
2063
2048
 
2064
- if (content.includes('variant') && content.includes('Math.random')) {
2049
+ if (content.includes('if (') && /feature|experiment|rollout|beta/i.test(content)) {
2050
+ const hasFeatureFlagLib = content.includes('unleash') || content.includes('launchdarkly') || content.includes('flagsmith');
2051
+
2052
+ if (!hasFeatureFlagLib) {
2065
2053
  pushFinding(
2066
- "frontend.devops.manual_ab_testing",
2054
+ "frontend.devops.hardcoded_feature_flag",
2067
2055
  "low",
2068
2056
  sf,
2069
2057
  sf,
2070
- 'Manual A/B testing with Math.random. Use: Google Optimize, Optimizely, VWO. Benefits: Statistical significance, reporting, targeting rules.',
2058
+ 'Hardcoded feature flag. Use feature flag service: Unleash, LaunchDarkly, Flagsmith. Benefits: Toggle features without deployment, gradual rollout, A/B testing.',
2071
2059
  findings
2072
2060
  );
2073
2061
  }
2062
+ }
2074
2063
 
2075
- if (content.includes('fetch') && !content.includes('cache') && !content.includes('revalidate')) {
2076
- const isFetchInServerComponent = content.includes('async function') && !content.includes('\'use client\'');
2064
+ if (content.includes('variant') && content.includes('Math.random')) {
2065
+ pushFinding(
2066
+ "frontend.devops.manual_ab_testing",
2067
+ "low",
2068
+ sf,
2069
+ sf,
2070
+ 'Manual A/B testing with Math.random. Use: Google Optimize, Optimizely, VWO. Benefits: Statistical significance, reporting, targeting rules.',
2071
+ findings
2072
+ );
2073
+ }
2077
2074
 
2078
- if (isFetchInServerComponent) {
2079
- pushFinding(
2080
- "frontend.performance.missing_cache_strategy",
2081
- "low",
2082
- sf,
2083
- sf,
2084
- 'Fetch without cache strategy. Add: { cache: \'force-cache\' } or { next: { revalidate: 3600 } }. Default behavior may change. Be explicit.',
2085
- findings
2086
- );
2087
- }
2075
+ if (content.includes('fetch') && !content.includes('cache') && !content.includes('revalidate')) {
2076
+ const isFetchInServerComponent = content.includes('async function') && !content.includes('\'use client\'');
2077
+
2078
+ if (isFetchInServerComponent) {
2079
+ pushFinding(
2080
+ "frontend.performance.missing_cache_strategy",
2081
+ "low",
2082
+ sf,
2083
+ sf,
2084
+ 'Fetch without cache strategy. Add: { cache: \'force-cache\' } or { next: { revalidate: 3600 } }. Default behavior may change. Be explicit.',
2085
+ findings
2086
+ );
2088
2087
  }
2088
+ }
2089
2089
  });
2090
2090
  }
2091
2091
 
@@ -0,0 +1,66 @@
1
+ const { iOSASTIntelligentAnalyzer } = require('../iOSASTIntelligentAnalyzer');
2
+
3
+ describe('iOSASTIntelligentAnalyzer - event-driven navigation rules', () => {
4
+ const makeSUT = () => {
5
+ const findings = [];
6
+ const sut = new iOSASTIntelligentAnalyzer(findings);
7
+ sut.fileContent = '';
8
+ sut.syntaxTokens = [];
9
+ sut.imports = [];
10
+ sut.classes = [];
11
+ sut.structs = [];
12
+ return { sut, findings };
13
+ };
14
+
15
+ const identifierToken = (value, offset = 0) => ({
16
+ kind: 'source.lang.swift.syntaxtype.identifier',
17
+ value,
18
+ offset,
19
+ length: value.length,
20
+ });
21
+
22
+ it('should report CRITICAL when UIKit imperative navigation is detected', () => {
23
+ const { sut, findings } = makeSUT();
24
+ sut.fileContent = 'pushViewController';
25
+ sut.syntaxTokens = [identifierToken('pushViewController', 0)];
26
+
27
+ sut.analyzeAdditionalRules('/tmp/File.swift');
28
+
29
+ const rule = findings.find((f) => f.ruleId === 'ios.navigation.imperative_navigation');
30
+ expect(rule).toBeDefined();
31
+ expect(String(rule.severity).toLowerCase()).toBe('critical');
32
+ });
33
+
34
+ it('should report CRITICAL when SwiftUI navigation API is detected outside View types', () => {
35
+ const { sut, findings } = makeSUT();
36
+ sut.fileContent = 'NavigationLink';
37
+ sut.syntaxTokens = [identifierToken('NavigationLink', 0)];
38
+ sut.imports = [];
39
+ sut.classes = [];
40
+ sut.structs = [];
41
+
42
+ sut.analyzeAdditionalRules('/tmp/NotAView.swift');
43
+
44
+ const rule = findings.find((f) => f.ruleId === 'ios.navigation.swiftui_navigation_outside_view');
45
+ expect(rule).toBeDefined();
46
+ expect(String(rule.severity).toLowerCase()).toBe('critical');
47
+ });
48
+
49
+ it('should not report SwiftUI navigation outside View when file is a SwiftUI View', () => {
50
+ const { sut, findings } = makeSUT();
51
+ sut.fileContent = 'import SwiftUI\nstruct MyView: View { var body: some View { NavigationLink("x", destination: Text("y")) } }';
52
+ sut.syntaxTokens = [identifierToken('NavigationLink', 0)];
53
+ sut.imports = [{ name: 'SwiftUI', line: 1 }];
54
+ sut.structs = [
55
+ {
56
+ 'key.name': 'MyView',
57
+ 'key.inheritedtypes': [{ 'key.name': 'View' }],
58
+ },
59
+ ];
60
+
61
+ sut.analyzeAdditionalRules('/tmp/MyView.swift');
62
+
63
+ const rule = findings.find((f) => f.ruleId === 'ios.navigation.swiftui_navigation_outside_view');
64
+ expect(rule).toBeUndefined();
65
+ });
66
+ });
@@ -815,9 +815,8 @@ class iOSASTIntelligentAnalyzer {
815
815
  return 0.6745 * (x - med) / madValue;
816
816
  };
817
817
 
818
- const env = require('../../../../config/env');
819
- const pOutlier = env.getNumber('AST_GODCLASS_P_OUTLIER', 90);
820
- const pExtreme = env.getNumber('AST_GODCLASS_P_EXTREME', 97);
818
+ const pOutlier = Number(process.env.AST_GODCLASS_P_OUTLIER || 90);
819
+ const pExtreme = Number(process.env.AST_GODCLASS_P_EXTREME || 97);
821
820
 
822
821
  const methods = this.godClassCandidates.map(c => c.methodsCount);
823
822
  const props = this.godClassCandidates.map(c => c.propertiesCount);