NSGIA Memory System

Free to use - Try Now | Home
Permanent Link Access
🔗 Permanent Link - This link never expires and can be shared freely
This is the permanent version of the temporary URLs the Memory System produces. Many AIs can directly ingest this format. If not, you can paste the JSON instead, or click on the Formatted view for easier reading — though AI tools generally work best with this version.
Some AIs cannot follow links. If that happens, click Copy JSON and paste it into the AI manually.
NSG Linter 2026-01-02 14:41:53

Full Content

TITLE: NSG Linter #!/usr/bin/env python3 # CHANGE LOG # File: nsg_linter.py # Purpose: Check code files against NSG standards # Dependencies: None (standard library only) # Usage: python nsg_linter.py [directory] # Version History: # 2025-01-02 v1.0 - Initial creation import os import re import sys from pathlib import Path from datetime import datetime # File length limits LIMITS = { '.py': {'soft': 400, 'hard': 600}, '.js': {'soft': 300, 'hard': 500}, '.html': {'soft': 200, 'hard': 300}, '.css': {'soft': 200, 'hard': 400}, } # Directories to skip SKIP_DIRS = {'venv', 'env', '.git', '__pycache__', 'node_modules', 'build', 'dist'} def count_lines(filepath): try: with open(filepath, 'r', encoding='utf-8') as f: return len(f.readlines()) except Exception as e: print(f"ERROR: count_lines - Cannot read {filepath}: {e}") return 0 def check_changelog(filepath, ext): try: with open(filepath, 'r', encoding='utf-8') as f: content = f.read(2000) if ext == '.py': if '# CHANGE LOG' in content or '# File:' in content: return True elif ext == '.html': if 'CHANGE LOG' in content or 'File:' in content: return True elif ext in ['.js', '.css']: if 'CHANGE LOG' in content or 'File:' in content: return True return False except Exception as e: print(f"ERROR: check_changelog - Cannot read {filepath}: {e}") return False def check_python_classes(filepath): classes_found = [] try: with open(filepath, 'r', encoding='utf-8') as f: lines = f.readlines() for i, line in enumerate(lines, 1): if re.match(r'^class\s+\w+', line.strip()): classes_found.append(i) return classes_found except Exception as e: print(f"ERROR: check_python_classes - Cannot read {filepath}: {e}") return [] def check_python_try_except(filepath): functions_without_try = [] try: with open(filepath, 'r', encoding='utf-8') as f: content = f.read() lines = content.split('\n') in_function = False current_function = None current_function_line = 0 has_try = False indent_level = 0 for i, line in enumerate(lines, 1): stripped = line.strip() if stripped.startswith('def ') and '(' in stripped: if in_function and not has_try and current_function: if not current_function.startswith('_'): functions_without_try.append((current_function_line, current_function)) match = re.match(r'def\s+(\w+)\s*\(', stripped) if match: current_function = match.group(1) current_function_line = i in_function = True has_try = False indent_level = len(line) - len(line.lstrip()) elif in_function and stripped.startswith('try:'): has_try = True if in_function and not has_try and current_function: if not current_function.startswith('_'): functions_without_try.append((current_function_line, current_function)) return functions_without_try except Exception as e: print(f"ERROR: check_python_try_except - Cannot read {filepath}: {e}") return [] def check_jinja_in_comments(filepath): violations = [] try: with open(filepath, 'r', encoding='utf-8') as f: content = f.read() in_comment = False lines = content.split('\n') for i, line in enumerate(lines, 1): if '<!--' in line: in_comment = True if in_comment: if re.search(r'\{%.*%\}', line) or re.search(r'\{\{.*\}\}', line): violations.append(i) if '-->' in line: in_comment = False return violations except Exception as e: print(f"ERROR: check_jinja_in_comments - Cannot read {filepath}: {e}") return [] def check_debug_error_prints(filepath): functions_without_prints = [] try: with open(filepath, 'r', encoding='utf-8') as f: content = f.read() function_pattern = r'def\s+(\w+)\s*\([^)]*\):' matches = list(re.finditer(function_pattern, content)) for i, match in enumerate(matches): func_name = match.group(1) if func_name.startswith('_'): continue start = match.end() if i + 1 < len(matches): end = matches[i + 1].start() else: end = len(content) func_body = content[start:end] has_except = 'except' in func_body if has_except: has_error_print = 'print(f"ERROR:' in func_body or "print(f'ERROR:" in func_body or 'print("ERROR:' in func_body if not has_error_print: line_num = content[:match.start()].count('\n') + 1 functions_without_prints.append((line_num, func_name)) return functions_without_prints except Exception as e: print(f"ERROR: check_debug_error_prints - Cannot read {filepath}: {e}") return [] def lint_file(filepath): issues = [] ext = Path(filepath).suffix.lower() if ext not in LIMITS: return issues line_count = count_lines(filepath) soft = LIMITS[ext]['soft'] hard = LIMITS[ext]['hard'] if line_count > hard: issues.append(f"ERROR: {line_count} lines exceeds hard limit of {hard}") elif line_count > soft: issues.append(f"WARNING: {line_count} lines exceeds soft limit of {soft}") if not check_changelog(filepath, ext): issues.append("ERROR: Missing change log header") if ext == '.py': classes = check_python_classes(filepath) for line_num in classes: issues.append(f"WARNING: Class definition at line {line_num} (no-classes rule)") funcs_no_try = check_python_try_except(filepath) for line_num, func_name in funcs_no_try: issues.append(f"WARNING: Function '{func_name}' at line {line_num} has no try/except") funcs_no_print = check_debug_error_prints(filepath) for line_num, func_name in funcs_no_print: issues.append(f"WARNING: Function '{func_name}' at line {line_num} missing ERROR: print in except block") if ext == '.html': jinja_violations = check_jinja_in_comments(filepath) for line_num in jinja_violations: issues.append(f"ERROR: Jinja syntax in HTML comment at line {line_num}") return issues def lint_directory(directory): print("=" * 60) print(f"NSG LINTER - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print(f"Scanning: {directory}") print("=" * 60) print() total_files = 0 files_with_issues = 0 total_errors = 0 total_warnings = 0 for root, dirs, files in os.walk(directory): dirs[:] = [d for d in dirs if d not in SKIP_DIRS] for filename in files: ext = Path(filename).suffix.lower() if ext not in LIMITS: continue filepath = os.path.join(root, filename) relative_path = os.path.relpath(filepath, directory) issues = lint_file(filepath) total_files += 1 if issues: files_with_issues += 1 print(f"--- {relative_path} ---") for issue in issues: print(f" {issue}") if issue.startswith("ERROR"): total_errors += 1 elif issue.startswith("WARNING"): total_warnings += 1 print() print("=" * 60) print("SUMMARY") print("=" * 60) print(f"Files scanned: {total_files}") print(f"Files with issues: {files_with_issues}") print(f"Total errors: {total_errors}") print(f"Total warnings: {total_warnings}") print() if total_errors > 0: print("RESULT: FAIL (errors found)") return 1 elif total_warnings > 0: print("RESULT: PASS with warnings") return 0 else: print("RESULT: PASS") return 0 def main(): if len(sys.argv) > 1: directory = sys.argv[1] else: directory = '.' if not os.path.isdir(directory): print(f"ERROR: {directory} is not a valid directory") sys.exit(1) result = lint_directory(directory) sys.exit(result) if __name__ == '__main__': main()