torusguard 0.9.4 → 1.0.0

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 (44) hide show
  1. package/.torusguard/.manifest.json +15 -10
  2. package/.torusguard/config/torusguard.json +1 -1
  3. package/.torusguard/schemas/memory-context.schema.json +95 -0
  4. package/.torusguard/schemas/memory-event.schema.json +70 -0
  5. package/.torusguard/schemas/memory-pattern.schema.json +65 -0
  6. package/.torusguard/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  7. package/.torusguard/scripts/__pycache__/finding_scorer.cpython-311.pyc +0 -0
  8. package/.torusguard/scripts/__pycache__/memory_engine.cpython-311.pyc +0 -0
  9. package/.torusguard/scripts/diff_guard.py +71 -11
  10. package/.torusguard/scripts/finding_scorer.py +171 -86
  11. package/.torusguard/scripts/manifest_builder.py +2 -0
  12. package/.torusguard/scripts/memory_engine.py +912 -0
  13. package/.torusguard/scripts/run_manager.py +171 -89
  14. package/.torusguard/skills/torusguard/SKILL.md +12 -7
  15. package/.torusguard/skills/torusguard/bootstrap.py +344 -56
  16. package/.torusguard/skills/torusguard-audit/SKILL.md +25 -25
  17. package/.torusguard/skills/torusguard-harden/SKILL.md +3 -3
  18. package/.torusguard/skills/torusguard-recheck/SKILL.md +3 -3
  19. package/.torusguard/workflows/memory.md +53 -0
  20. package/README.md +12 -2
  21. package/bin/torusguard.js +173 -43
  22. package/package.json +1 -1
  23. package/skills/torusguard/SKILL.md +4 -3
  24. package/skills/torusguard/__pycache__/bootstrap.cpython-311.pyc +0 -0
  25. package/skills/torusguard/bootstrap.py +244 -80
  26. package/skills/torusguard/payload/.manifest.json +15 -10
  27. package/skills/torusguard/payload/config/torusguard.json +1 -1
  28. package/skills/torusguard/payload/schemas/memory-context.schema.json +95 -0
  29. package/skills/torusguard/payload/schemas/memory-event.schema.json +70 -0
  30. package/skills/torusguard/payload/schemas/memory-pattern.schema.json +65 -0
  31. package/skills/torusguard/payload/scripts/diff_guard.py +71 -11
  32. package/skills/torusguard/payload/scripts/finding_scorer.py +171 -86
  33. package/skills/torusguard/payload/scripts/manifest_builder.py +2 -0
  34. package/skills/torusguard/payload/scripts/memory_engine.py +912 -0
  35. package/skills/torusguard/payload/scripts/run_manager.py +171 -89
  36. package/skills/torusguard/payload/skills/torusguard/SKILL.md +12 -7
  37. package/skills/torusguard/payload/skills/torusguard/bootstrap.py +344 -56
  38. package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +25 -25
  39. package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +3 -3
  40. package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +3 -3
  41. package/skills/torusguard/payload/workflows/memory.md +53 -0
  42. package/skills/torusguard-audit/SKILL.md +25 -25
  43. package/skills/torusguard-harden/SKILL.md +3 -3
  44. package/skills/torusguard-recheck/SKILL.md +3 -3
@@ -0,0 +1,70 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "torusguard-memory-event.schema.json",
4
+ "title": "TorusGuard Memory Event",
5
+ "description": "Schema for individual memory events recorded during audit/fix/recheck cycles.",
6
+ "type": "object",
7
+ "required": ["event_id", "event_type", "timestamp", "version"],
8
+ "properties": {
9
+ "event_id": {
10
+ "type": "string",
11
+ "description": "Unique event identifier (UUID v4 or timestamp-based)."
12
+ },
13
+ "event_type": {
14
+ "type": "string",
15
+ "enum": ["audit_finding", "fix_applied", "fix_verified", "false_positive", "pattern_learned", "stack_changed"],
16
+ "description": "Category of security event."
17
+ },
18
+ "timestamp": {
19
+ "type": "string",
20
+ "format": "date-time",
21
+ "description": "ISO 8601 UTC timestamp."
22
+ },
23
+ "version": {
24
+ "type": "string",
25
+ "description": "TorusGuard version that recorded this event."
26
+ },
27
+ "rule_id": {
28
+ "type": ["string", "null"],
29
+ "description": "TorusGuard rule ID (e.g., TG-DB-004)."
30
+ },
31
+ "file_path": {
32
+ "type": ["string", "null"],
33
+ "description": "Relative file path within the project."
34
+ },
35
+ "line_number": {
36
+ "type": ["integer", "null"],
37
+ "description": "Line number of the finding."
38
+ },
39
+ "severity": {
40
+ "type": ["string", "null"],
41
+ "enum": ["critical", "high", "medium", "low", "info", null]
42
+ },
43
+ "confidence_score": {
44
+ "type": ["integer", "null"],
45
+ "minimum": 0,
46
+ "maximum": 100
47
+ },
48
+ "code_hash": {
49
+ "type": ["string", "null"],
50
+ "description": "SHA-256 hash of the relevant code snippet for pattern matching."
51
+ },
52
+ "fix_strategy": {
53
+ "type": ["string", "null"],
54
+ "description": "Human-readable description of the fix approach used."
55
+ },
56
+ "verification_result": {
57
+ "type": ["string", "null"],
58
+ "enum": ["fixed", "regressed", "partial", "not_tested", null]
59
+ },
60
+ "suppression_reason": {
61
+ "type": ["string", "null"],
62
+ "description": "Reason for false positive suppression."
63
+ },
64
+ "metadata": {
65
+ "type": "object",
66
+ "description": "Additional event-specific metadata.",
67
+ "additionalProperties": true
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "torusguard-memory-pattern.schema.json",
4
+ "title": "TorusGuard Memory Pattern",
5
+ "description": "Schema for distilled security patterns extracted from raw events. Patterns represent recurring fix strategies, vulnerability classes, and project-specific security idioms.",
6
+ "type": "object",
7
+ "required": ["pattern_id", "rule_id", "pattern_type", "confidence", "occurrences", "first_seen", "last_seen"],
8
+ "properties": {
9
+ "pattern_id": {
10
+ "type": "string",
11
+ "description": "Unique pattern identifier (PAT-NNN)."
12
+ },
13
+ "rule_id": {
14
+ "type": "string",
15
+ "description": "TorusGuard rule ID this pattern relates to."
16
+ },
17
+ "pattern_type": {
18
+ "type": "string",
19
+ "enum": ["recurring_fix", "common_vulnerability", "false_positive_class", "regression_watch", "security_idiom"],
20
+ "description": "Category of the distilled pattern."
21
+ },
22
+ "description": {
23
+ "type": "string",
24
+ "description": "Human-readable summary of the pattern."
25
+ },
26
+ "fix_strategy": {
27
+ "type": ["string", "null"],
28
+ "description": "The preferred fix approach observed across multiple occurrences."
29
+ },
30
+ "confidence": {
31
+ "type": "integer",
32
+ "minimum": 0,
33
+ "maximum": 100,
34
+ "description": "Pattern confidence (amplified by repeated confirmations, decayed by TTL)."
35
+ },
36
+ "occurrences": {
37
+ "type": "integer",
38
+ "minimum": 1,
39
+ "description": "Number of times this pattern has been observed."
40
+ },
41
+ "affected_files": {
42
+ "type": "array",
43
+ "items": { "type": "string" },
44
+ "description": "List of relative file paths where this pattern has been observed."
45
+ },
46
+ "first_seen": {
47
+ "type": "string",
48
+ "format": "date-time"
49
+ },
50
+ "last_seen": {
51
+ "type": "string",
52
+ "format": "date-time"
53
+ },
54
+ "decay_checkpoint": {
55
+ "type": ["string", "null"],
56
+ "format": "date-time",
57
+ "description": "Last timestamp when this pattern's confidence was reconfirmed or decayed."
58
+ },
59
+ "source_events": {
60
+ "type": "array",
61
+ "items": { "type": "string" },
62
+ "description": "List of event_ids that contributed to this pattern."
63
+ }
64
+ }
65
+ }
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- TorusGuard Content-Aware Diff Line Scanner (v0.9.3)
4
- Evaluates unified diff patches against security invariants before harden or apply.
3
+ TorusGuard Content-Aware Diff Line Scanner (v1.0.0)
4
+ Evaluates unified diff patches against security invariants before harden or apply,
5
+ including regression watch checks against persistent memory patterns.
5
6
  Pure Python 3.10+ standard library (zero external dependencies).
6
7
  """
7
8
 
@@ -38,16 +39,69 @@ TENANT_FILTER_PATTERNS = [
38
39
  ]
39
40
 
40
41
 
41
- def audit_diff(diff_content: str) -> Dict[str, Any]:
42
+ def check_memory_regressions(
43
+ current_file: str,
44
+ current_line: int,
45
+ added_content: str,
46
+ root_dir: Optional[Path] = None
47
+ ) -> List[Dict[str, Any]]:
48
+ """
49
+ Check if the added line re-introduces a vulnerability in a file
50
+ that is tracked under active Regression Watch in .torusguard/memory/patterns.json.
51
+ """
52
+ violations = []
53
+ base = Path(root_dir or Path.cwd()).resolve()
54
+ torusguard_dir = base / ".torusguard"
55
+ if not torusguard_dir.exists():
56
+ for parent in base.parents:
57
+ if (parent / ".torusguard").exists():
58
+ torusguard_dir = parent / ".torusguard"
59
+ break
60
+
61
+ patterns_file = torusguard_dir / "memory" / "patterns.json"
62
+ if not patterns_file.exists():
63
+ return violations
64
+
65
+ try:
66
+ with open(patterns_file, "r", encoding="utf-8") as f:
67
+ patterns = json.load(f)
68
+ except Exception:
69
+ return violations
70
+
71
+ clean_file = current_file.replace("\\", "/")
72
+ for pat in patterns:
73
+ if pat.get("pattern_type") == "regression_watch":
74
+ affected = [f.replace("\\", "/") for f in pat.get("affected_files", [])]
75
+ if clean_file in affected or any(clean_file.endswith(a) for a in affected):
76
+ rule_id = pat.get("rule_id", "TG-REG")
77
+ desc = pat.get("description", "Regression watch violation")
78
+ violations.append({
79
+ "rule_id": "TG-DIFF-004",
80
+ "category": "Regression Watch",
81
+ "severity": "HIGH",
82
+ "file": current_file,
83
+ "line": current_line,
84
+ "content": added_content.strip(),
85
+ "description": f"File {current_file} matches active Regression Watch for {rule_id}: {desc}"
86
+ })
87
+ return violations
88
+
89
+
90
+ def audit_diff(
91
+ diff_content: str,
92
+ check_memory: bool = False,
93
+ root_dir: Optional[Path] = None
94
+ ) -> Dict[str, Any]:
42
95
  """
43
96
  Parses a unified diff and returns detected violations.
97
+ Optionally checks against active regression watch patterns in memory.
44
98
  """
45
99
  lines = diff_content.splitlines()
46
100
  violations: List[Dict[str, Any]] = []
47
-
101
+
48
102
  current_file = "unknown"
49
103
  current_line = 0
50
-
104
+
51
105
  additions: List[tuple[int, str]] = []
52
106
  deletions: List[tuple[int, str]] = []
53
107
 
@@ -68,7 +122,7 @@ def audit_diff(diff_content: str) -> Dict[str, Any]:
68
122
  current_line += 1
69
123
  added_content = line[1:]
70
124
  additions.append((current_line, added_content))
71
-
125
+
72
126
  # Check bypass patterns
73
127
  for pat, desc in BYPASS_PATTERNS:
74
128
  if pat.search(added_content):
@@ -81,7 +135,7 @@ def audit_diff(diff_content: str) -> Dict[str, Any]:
81
135
  "content": added_content.strip(),
82
136
  "description": desc
83
137
  })
84
-
138
+
85
139
  # Check secret ingestion
86
140
  for pat, desc in SECRET_PATTERNS:
87
141
  if pat.search(added_content):
@@ -95,6 +149,11 @@ def audit_diff(diff_content: str) -> Dict[str, Any]:
95
149
  "description": desc
96
150
  })
97
151
 
152
+ # Check memory regression watch if requested
153
+ if check_memory:
154
+ reg_violations = check_memory_regressions(current_file, current_line, added_content, root_dir=root_dir)
155
+ violations.extend(reg_violations)
156
+
98
157
  elif line.startswith('-') and not line.startswith('---'):
99
158
  deleted_content = line[1:]
100
159
  deletions.append((line_idx, deleted_content))
@@ -127,7 +186,7 @@ def audit_diff(diff_content: str) -> Dict[str, Any]:
127
186
  }
128
187
 
129
188
 
130
- def audit_diff_file(file_path: str) -> Dict[str, Any]:
189
+ def audit_diff_file(file_path: str, check_memory: bool = False, root_dir: Optional[Path] = None) -> Dict[str, Any]:
131
190
  path = Path(file_path)
132
191
  if not path.is_file():
133
192
  return {
@@ -136,23 +195,24 @@ def audit_diff_file(file_path: str) -> Dict[str, Any]:
136
195
  "violations": []
137
196
  }
138
197
  content = path.read_text(encoding="utf-8", errors="replace")
139
- return audit_diff(content)
198
+ return audit_diff(content, check_memory=check_memory, root_dir=root_dir)
140
199
 
141
200
 
142
201
  def main():
143
202
  parser = argparse.ArgumentParser(description="TorusGuard Unified Diff Security Scanner")
144
203
  parser.add_argument("diff_path", nargs="?", help="Path to unified diff file (or read stdin if omitted)")
204
+ parser.add_argument("--check-memory", action="store_true", help="Check additions against persistent memory regression watch patterns")
145
205
  parser.add_argument("--json", action="store_true", help="Output results as JSON")
146
206
  args = parser.parse_args()
147
207
 
148
208
  if args.diff_path:
149
- res = audit_diff_file(args.diff_path)
209
+ res = audit_diff_file(args.diff_path, check_memory=args.check_memory)
150
210
  else:
151
211
  if sys.stdin.isatty():
152
212
  parser.print_help()
153
213
  sys.exit(1)
154
214
  diff_text = sys.stdin.read()
155
- res = audit_diff(diff_text)
215
+ res = audit_diff(diff_text, check_memory=args.check_memory)
156
216
 
157
217
  if args.json:
158
218
  print(json.dumps(res, indent=2))
@@ -1,86 +1,171 @@
1
- #!/usr/bin/env python3
2
- """
3
- TorusGuard 5-Factor Finding Confidence Scorer
4
- Evaluates finding confidence against the objective 0–100 scoring model.
5
- """
6
-
7
- import sys
8
- import json
9
- import argparse
10
- from typing import Dict, Any, Tuple
11
-
12
- def compute_confidence_score(
13
- evidence_quality: int = 35,
14
- reproduction_success: int = 0,
15
- independent_confirmations: int = 5,
16
- environmental_clarity: int = 15,
17
- manual_review_status: int = 0
18
- ) -> Tuple[int, str, Dict[str, int]]:
19
- """
20
- Computes total score and assigns confidence band.
21
- Max points:
22
- - evidence_quality: 35
23
- - reproduction_success: 25
24
- - independent_confirmations: 15
25
- - environmental_clarity: 15
26
- - manual_review_status: 10
27
- """
28
- eq = min(max(evidence_quality, 0), 35)
29
- rs = min(max(reproduction_success, 0), 25)
30
- ic = min(max(independent_confirmations, 0), 15)
31
- ec = min(max(environmental_clarity, 0), 15)
32
- mr = min(max(manual_review_status, 0), 10)
33
-
34
- total = eq + rs + ic + ec + mr
35
-
36
- if total >= 90:
37
- band = "Confirmed"
38
- elif total >= 70:
39
- band = "High Confidence"
40
- elif total >= 50:
41
- band = "Medium Confidence"
42
- else:
43
- band = "Needs Review"
44
-
45
- factors = {
46
- "evidence_quality": eq,
47
- "reproduction_success": rs,
48
- "independent_confirmations": ic,
49
- "environmental_clarity": ec,
50
- "manual_review_status": mr,
51
- "total_score": total,
52
- "classification_band": band
53
- }
54
- return total, band, factors
55
-
56
-
57
- def main():
58
- parser = argparse.ArgumentParser(description="TorusGuard Confidence Scorer")
59
- parser.add_argument("--eq", type=int, default=35, help="Evidence quality score (0-35)")
60
- parser.add_argument("--rs", type=int, default=0, help="Reproduction success score (0-25)")
61
- parser.add_argument("--ic", type=int, default=5, help="Independent confirmations score (0-15)")
62
- parser.add_argument("--ec", type=int, default=15, help="Environmental clarity score (0-15)")
63
- parser.add_argument("--mr", type=int, default=0, help="Manual review status score (0-10)")
64
- parser.add_argument("--json", action="store_true", help="Output JSON")
65
- args = parser.parse_args()
66
-
67
- total, band, factors = compute_confidence_score(
68
- evidence_quality=args.eq,
69
- reproduction_success=args.rs,
70
- independent_confirmations=args.ic,
71
- environmental_clarity=args.ec,
72
- manual_review_status=args.mr
73
- )
74
-
75
- if args.json:
76
- print(json.dumps(factors, indent=2))
77
- else:
78
- print(f"Total Confidence Score: {total}/100")
79
- print(f"Classification Band: {band}")
80
- print("Factor Breakdown:")
81
- for k, v in factors.items():
82
- if k not in ("total_score", "classification_band"):
83
- print(f" - {k}: {v}")
84
-
85
- if __name__ == "__main__":
86
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ TorusGuard Finding Confidence Scorer (v1.0.0)
4
+ Evaluates finding confidence against the objective 0–100 scoring model,
5
+ augmented by persistent memory patterns and false-positive suppression.
6
+ """
7
+
8
+ import sys
9
+ import json
10
+ import argparse
11
+ from pathlib import Path
12
+ from typing import Dict, Any, Tuple, Optional
13
+
14
+
15
+ def compute_memory_boost(
16
+ rule_id: str,
17
+ file_path: Optional[str] = None,
18
+ root_dir: Optional[Path] = None
19
+ ) -> int:
20
+ """
21
+ Query .torusguard/memory/patterns.json to calculate memory confidence modifier:
22
+ - False positive class: -30 (heavily suppressed)
23
+ - Regression watch: +15 (high alert / known recurrence)
24
+ - Recurring fix / Common vulnerability: +10
25
+ - Security idiom: +5
26
+ - Multi-file pattern match bonus: +5
27
+ """
28
+ if not rule_id:
29
+ return 0
30
+
31
+ base = Path(root_dir or Path.cwd()).resolve()
32
+ # Search for .torusguard
33
+ torusguard_dir = base / ".torusguard"
34
+ if not torusguard_dir.exists():
35
+ for parent in base.parents:
36
+ if (parent / ".torusguard").exists():
37
+ torusguard_dir = parent / ".torusguard"
38
+ break
39
+
40
+ patterns_file = torusguard_dir / "memory" / "patterns.json"
41
+ if not patterns_file.exists():
42
+ return 0
43
+
44
+ try:
45
+ with open(patterns_file, "r", encoding="utf-8") as f:
46
+ patterns = json.load(f)
47
+ except Exception:
48
+ return 0
49
+
50
+ boost = 0
51
+ matched_file = False
52
+
53
+ for pat in patterns:
54
+ if pat.get("rule_id") == rule_id:
55
+ ptype = pat.get("pattern_type")
56
+ if ptype == "false_positive_class":
57
+ boost -= 30
58
+ elif ptype == "regression_watch":
59
+ boost += 15
60
+ elif ptype in ("recurring_fix", "common_vulnerability"):
61
+ boost += 10
62
+ elif ptype == "security_idiom":
63
+ boost += 5
64
+
65
+ # File-specific correlation bonus
66
+ if file_path and file_path in pat.get("affected_files", []):
67
+ matched_file = True
68
+
69
+ if matched_file and boost > 0:
70
+ boost += 5
71
+ elif matched_file and boost < 0:
72
+ boost -= 10
73
+
74
+ return boost
75
+
76
+
77
+ def compute_confidence_score(
78
+ evidence_quality: int = 35,
79
+ reproduction_success: int = 0,
80
+ independent_confirmations: int = 5,
81
+ environmental_clarity: int = 15,
82
+ manual_review_status: int = 0,
83
+ memory_boost: int = 0,
84
+ rule_id: Optional[str] = None,
85
+ file_path: Optional[str] = None,
86
+ root_dir: Optional[Path] = None
87
+ ) -> Tuple[int, str, Dict[str, int]]:
88
+ """
89
+ Computes total score and assigns confidence band.
90
+ Max points:
91
+ - evidence_quality: 35
92
+ - reproduction_success: 25
93
+ - independent_confirmations: 15
94
+ - environmental_clarity: 15
95
+ - manual_review_status: 10
96
+ - memory_boost: -30 to +20 (modifier from persistent memory)
97
+ Total is clamped to [0, 100].
98
+ """
99
+ eq = min(max(evidence_quality, 0), 35)
100
+ rs = min(max(reproduction_success, 0), 25)
101
+ ic = min(max(independent_confirmations, 0), 15)
102
+ ec = min(max(environmental_clarity, 0), 15)
103
+ mr = min(max(manual_review_status, 0), 10)
104
+
105
+ # Auto-calculate memory boost if rule_id provided and memory_boost == 0
106
+ eff_mem_boost = memory_boost
107
+ if rule_id and eff_mem_boost == 0:
108
+ eff_mem_boost = compute_memory_boost(rule_id, file_path=file_path, root_dir=root_dir)
109
+
110
+ raw_total = eq + rs + ic + ec + mr + eff_mem_boost
111
+ total = min(max(raw_total, 0), 100)
112
+
113
+ if total >= 90:
114
+ band = "Confirmed"
115
+ elif total >= 70:
116
+ band = "High Confidence"
117
+ elif total >= 50:
118
+ band = "Medium Confidence"
119
+ else:
120
+ band = "Needs Review"
121
+
122
+ factors = {
123
+ "evidence_quality": eq,
124
+ "reproduction_success": rs,
125
+ "independent_confirmations": ic,
126
+ "environmental_clarity": ec,
127
+ "manual_review_status": mr,
128
+ "memory_boost": eff_mem_boost,
129
+ "total_score": total,
130
+ "classification_band": band
131
+ }
132
+ return total, band, factors
133
+
134
+
135
+ def main():
136
+ parser = argparse.ArgumentParser(description="TorusGuard Confidence Scorer")
137
+ parser.add_argument("--eq", type=int, default=35, help="Evidence quality score (0-35)")
138
+ parser.add_argument("--rs", type=int, default=0, help="Reproduction success score (0-25)")
139
+ parser.add_argument("--ic", type=int, default=5, help="Independent confirmations score (0-15)")
140
+ parser.add_argument("--ec", type=int, default=15, help="Environmental clarity score (0-15)")
141
+ parser.add_argument("--mr", type=int, default=0, help="Manual review status score (0-10)")
142
+ parser.add_argument("--memory-boost", type=int, default=0, help="Memory boost score (-30 to +20)")
143
+ parser.add_argument("--rule-id", type=str, help="TorusGuard rule ID for memory pattern matching")
144
+ parser.add_argument("--file", type=str, help="File path for correlation matching")
145
+ parser.add_argument("--json", action="store_true", help="Output JSON")
146
+ args = parser.parse_args()
147
+
148
+ total, band, factors = compute_confidence_score(
149
+ evidence_quality=args.eq,
150
+ reproduction_success=args.rs,
151
+ independent_confirmations=args.ic,
152
+ environmental_clarity=args.ec,
153
+ manual_review_status=args.mr,
154
+ memory_boost=args.memory_boost,
155
+ rule_id=args.rule_id,
156
+ file_path=args.file
157
+ )
158
+
159
+ if args.json:
160
+ print(json.dumps(factors, indent=2))
161
+ else:
162
+ print(f"Total Confidence Score: {total}/100")
163
+ print(f"Classification Band: {band}")
164
+ print("Factor Breakdown:")
165
+ for k, v in factors.items():
166
+ if k not in ("total_score", "classification_band"):
167
+ print(f" - {k}: {v}")
168
+
169
+
170
+ if __name__ == "__main__":
171
+ main()
@@ -29,6 +29,8 @@ def scan_workspace_files(base_dir):
29
29
  for root, dirs, files in os.walk(base_dir):
30
30
  if "runs" in dirs:
31
31
  dirs.remove("runs")
32
+ if "memory" in dirs:
33
+ dirs.remove("memory")
32
34
  if "__pycache__" in dirs:
33
35
  dirs.remove("__pycache__")
34
36