@theglitchking/babel-fish 2.0.2 → 2.3.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 (45) hide show
  1. package/.claude/project-map/generate.py +206 -21
  2. package/.claude/project-map/grader.py +77 -1
  3. package/.claude/project-map/mine-sessions.py +141 -23
  4. package/.claude/project-map/test_generate.py +203 -0
  5. package/.claude/project-map/test_grader.py +164 -0
  6. package/.claude/project-map/test_mine_sessions.py +206 -0
  7. package/.claude-plugin/marketplace.json +2 -2
  8. package/.claude-plugin/plugin.json +1 -1
  9. package/CHANGELOG.md +209 -0
  10. package/README.md +9 -2
  11. package/checksums.json +2 -2
  12. package/hooks/session-start.js +34 -1
  13. package/package.json +13 -4
  14. package/.claude/project-map/PROJECT_MAP.md +0 -61
  15. package/.claude/project-map/__pycache__/generate.cpython-312.pyc +0 -0
  16. package/.claude/project-map/checksums.json +0 -6
  17. package/.claude/project-map/learned-vocabulary.json +0 -1
  18. package/.claude/project-map/reports/install-report.md +0 -54
  19. package/.claude/project-map/reports/iteration-01-report.md +0 -54
  20. package/.claude/project-map/reports/iteration-01-score.json +0 -7
  21. package/.claude/project-map/sections/01-vocabulary.md +0 -8
  22. package/.claude/project-map/sections/02-service-topology.md +0 -6
  23. package/.claude/project-map/sections/03-environment.md +0 -6
  24. package/.claude/project-map/sections/04-api-routes.md +0 -6
  25. package/.claude/project-map/sections/05-data-models.md +0 -4
  26. package/.claude/project-map/sections/06-schemas.md +0 -4
  27. package/.claude/project-map/sections/07-services.md +0 -6
  28. package/.claude/project-map/sections/08-background-jobs.md +0 -5
  29. package/.claude/project-map/sections/09-frontend-features.md +0 -4
  30. package/.claude/project-map/sections/10-tools-commands.md +0 -8
  31. package/.claude/project-map/sections/11-migrations.md +0 -4
  32. package/.claude/project-map/sections/12-import-chains.md +0 -7
  33. package/.claude/project-map/sections/13-frontend-backend-map.md +0 -8
  34. package/.claude/project-map/sections/14-reverse-proxy.md +0 -4
  35. package/.claude/project-map/sections/15-auth-config.md +0 -6
  36. package/.claude/project-map/sections/16-infra-profile.md +0 -13
  37. package/.claude/project-map/sections/17-learned-vocabulary.md +0 -7
  38. package/.claude/project-map/sections/18-dead-code.md +0 -9
  39. package/.claude/project-map/sections/19-doc-pointers.md +0 -5
  40. package/.claude/project-map/stack.json +0 -12
  41. package/.claude/rules/operational-runbook.md +0 -40
  42. package/.claude/rules/project-vocabulary.md +0 -25
  43. package/.claude/settings.json +0 -6
  44. package/.claude/settings.local.json +0 -6
  45. package/.claude/skills/babel-fish-developer-skill/SKILL.md +0 -56
@@ -13,6 +13,7 @@ import ast
13
13
  import argparse
14
14
  import hashlib
15
15
  import json
16
+ from fnmatch import fnmatch
16
17
  import re
17
18
  import subprocess
18
19
  import sys
@@ -53,13 +54,41 @@ def redact_secrets(text: str) -> str:
53
54
  # ── Checksum logic ───────────────────────────────────────────────────────────
54
55
  WATCHED_EXTENSIONS = {
55
56
  '.py', '.ts', '.tsx', '.js', '.jsx', '.go', '.java', '.kt',
56
- '.yaml', '.yml', '.toml', '.json', '.prisma', '.sql', '.env',
57
+ '.yaml', '.yml', '.toml', '.json', '.prisma', '.sql', '.env', '.sh',
57
58
  }
58
59
  WATCHED_NAMES = {
59
60
  'docker-compose.yml', 'docker-compose.yaml', 'docker-compose.dev.yml',
60
61
  'package.json', 'requirements.txt', 'pyproject.toml', 'go.mod',
61
62
  'Cargo.toml', 'pom.xml', 'Gemfile', 'Makefile',
62
63
  }
64
+ # Skill/command manifests — matched by path shape, not by extension, so
65
+ # documentation .md files stay out of the watch set. fnmatch runs against the
66
+ # repo-relative posix path, which anchors the glob at the root:
67
+ # ".documentation/reference/commands/cli.md" does not match "commands/*.md".
68
+ WATCHED_GLOBS = (
69
+ 'skills/*/SKILL.md',
70
+ 'commands/*.md',
71
+ # Dead until '.claude' leaves IGNORE_DIRS; kept as the anchor for when it does.
72
+ '.claude/skills/*/SKILL.md',
73
+ '.claude/commands/*.md',
74
+ )
75
+
76
+ # Directories build_doc_pointers_section() walks. Watched by PATH ONLY (no
77
+ # mtime) so adding or removing a doc refreshes section 19 while editing one
78
+ # does not churn the whole map.
79
+ DOC_DIRS = ['docs', 'doc', 'documentation', 'wiki', '.docs', '.documentation']
80
+ DOC_EXTS = {'.md', '.rst', '.txt', '.adoc'}
81
+ # Auto-generated navigation, not documentation. A hit-em-with-the-docs tree
82
+ # carries one INDEX.md + REGISTRY.md per domain (32 files for 15 domains), which
83
+ # would otherwise crowd every real doc out of section 19's 30-entry cap.
84
+ DOC_SKIP_NAMES = {'INDEX.md', 'REGISTRY.md'}
85
+ # hewtd excludes archive/ from all of its own scans; deprecated docs are not
86
+ # pointers worth handing an agent. 'reports' holds generated, timestamped audit
87
+ # output — listing it is noise, and because every run writes a NEW filename it
88
+ # would change the doc path set and force a full regeneration each time, which
89
+ # is the churn the path-only doc hash exists to prevent.
90
+ DOC_SKIP_DIRS = {'archive', 'reports'}
91
+
63
92
  IGNORE_DIRS = {
64
93
  '.git', 'node_modules', '__pycache__', '.venv', 'venv', 'env',
65
94
  'dist', 'build', '.next', '.nuxt', 'target', 'vendor', '.cache',
@@ -71,11 +100,34 @@ def collect_watched_files() -> list[Path]:
71
100
  for path in sorted(PROJECT_ROOT.rglob('*')):
72
101
  if any(p in IGNORE_DIRS for p in path.parts):
73
102
  continue
74
- if path.is_file() and (path.suffix in WATCHED_EXTENSIONS or path.name in WATCHED_NAMES):
103
+ if not path.is_file():
104
+ continue
105
+ rel = path.relative_to(PROJECT_ROOT).as_posix()
106
+ if (path.suffix in WATCHED_EXTENSIONS
107
+ or path.name in WATCHED_NAMES
108
+ or any(fnmatch(rel, g) for g in WATCHED_GLOBS)):
75
109
  result.append(path)
76
110
  return result
77
111
 
78
- def compute_checksum(files: list[Path]) -> str:
112
+
113
+ def collect_doc_files() -> list[Path]:
114
+ """Docs section 19 points at. Hashed by path only — see compute_checksum."""
115
+ docs = []
116
+ for doc_dir in DOC_DIRS:
117
+ d = PROJECT_ROOT / doc_dir
118
+ if d.is_dir():
119
+ docs.extend(
120
+ f for f in d.rglob('*')
121
+ if f.is_file()
122
+ and f.suffix in DOC_EXTS
123
+ and f.name not in DOC_SKIP_NAMES
124
+ and not (DOC_SKIP_DIRS & set(f.relative_to(PROJECT_ROOT).parts))
125
+ )
126
+ # Root-level docs, matching build_doc_pointers_section()'s own glob.
127
+ docs.extend(f for f in PROJECT_ROOT.glob('*.md') if f.is_file())
128
+ return sorted(set(docs))
129
+
130
+ def compute_checksum(files: list[Path], doc_files: list[Path] | None = None) -> str:
79
131
  h = hashlib.sha256()
80
132
  for f in files:
81
133
  h.update(str(f).encode())
@@ -83,6 +135,11 @@ def compute_checksum(files: list[Path]) -> str:
83
135
  h.update(str(f.stat().st_mtime_ns).encode())
84
136
  except OSError:
85
137
  pass
138
+ # ponytail: path only, no mtime — a new/renamed/deleted doc regenerates the
139
+ # section 19 pointer list, an edited one doesn't. Hashing doc mtimes would
140
+ # force a full regeneration on every prose edit.
141
+ for f in doc_files or []:
142
+ h.update(str(f).encode())
86
143
  return h.hexdigest()
87
144
 
88
145
  def load_checksums() -> dict:
@@ -710,9 +767,20 @@ class VocabularyBuilder:
710
767
  schemas: list[dict],
711
768
  features: list[dict],
712
769
  stack: dict,
770
+ skills: list[dict] | None = None,
713
771
  ) -> list[dict]:
714
772
  vocab: dict[str, dict] = {}
715
773
 
774
+ # From skills and slash commands. In a plugin repo this is the only
775
+ # source that fires — there are no routes or models to mine.
776
+ for sk in skills or []:
777
+ note = sk['description'][:120] if sk['description'] else f"{sk['kind']} manifest"
778
+ for alias in self._name_to_aliases(sk['name']):
779
+ self._add(vocab, alias, sk['kind'], sk['file'], note)
780
+ if sk['kind'] == 'command':
781
+ # humans say "/status" as often as "status"
782
+ self._add(vocab, f"/{sk['name']}", 'command', sk['file'], note)
783
+
716
784
  # From features
717
785
  for feat in features:
718
786
  name = feat['name']
@@ -781,6 +849,98 @@ class VocabularyBuilder:
781
849
  return {}
782
850
 
783
851
 
852
+ # ── Skill / Command Manifest Parser ───────────────────────────────────────────
853
+
854
+ class SkillParser:
855
+ """Claude skill and slash-command manifests.
856
+
857
+ In a plugin/skill repo these ARE the source: there are no routes or models
858
+ to extract, but a skill's frontmatter name + description is literally an
859
+ alias -> location pair, which is what section 01 wants.
860
+
861
+ The .claude/* patterns are globbed directly, the same way ToolsScanner
862
+ reaches .claude/skills, so a consuming project's installed skills are
863
+ picked up even though '.claude' is in IGNORE_DIRS. Those files are parsed
864
+ but not watched, so they refresh on the next regeneration rather than
865
+ immediately — see WATCHED_GLOBS.
866
+ """
867
+
868
+ PATTERNS = (
869
+ ('skills/*/SKILL.md', 'skill'),
870
+ ('.claude/skills/*/SKILL.md', 'skill'),
871
+ ('commands/*.md', 'command'),
872
+ ('.claude/commands/*.md', 'command'),
873
+ )
874
+
875
+ def parse(self) -> list[dict]:
876
+ found: dict[str, dict] = {}
877
+ for pattern, kind in self.PATTERNS:
878
+ for f in sorted(PROJECT_ROOT.glob(pattern)):
879
+ meta = self._frontmatter(f)
880
+ if meta is None:
881
+ continue
882
+ # commands carry no 'name:' — the filename is the command.
883
+ name = str(meta.get('name') or '').strip()
884
+ if not name:
885
+ name = f.parent.name if f.name == 'SKILL.md' else f.stem
886
+ desc = ' '.join(str(meta.get('description') or '').split())
887
+ rel = str(f.relative_to(PROJECT_ROOT))
888
+ found.setdefault(rel, {
889
+ 'name': name,
890
+ 'kind': kind,
891
+ 'file': rel,
892
+ 'description': desc,
893
+ })
894
+ return list(found.values())
895
+
896
+ def _frontmatter(self, f: Path) -> dict | None:
897
+ """Leading --- ... --- YAML block, or None when absent/unparseable."""
898
+ try:
899
+ text = f.read_text(encoding='utf-8', errors='replace')
900
+ except OSError:
901
+ return None
902
+ if not text.startswith('---'):
903
+ return None
904
+ end = text.find('\n---', 3)
905
+ if end == -1:
906
+ return None
907
+ block = text[3:end]
908
+ if HAS_YAML:
909
+ try:
910
+ data = yaml.safe_load(block)
911
+ if isinstance(data, dict):
912
+ return data
913
+ except Exception:
914
+ pass
915
+ return self._frontmatter_regex(block)
916
+
917
+ def _frontmatter_regex(self, block: str) -> dict:
918
+ """pyyaml-free fallback. Handles 'key: value' and 'key: |' blocks —
919
+ enough for name/description, which is all this parser reads."""
920
+ out: dict[str, str] = {}
921
+ lines = block.splitlines()
922
+ i = 0
923
+ while i < len(lines):
924
+ m = re.match(r'^([A-Za-z_][\w-]*):\s*(.*)$', lines[i])
925
+ if not m:
926
+ i += 1
927
+ continue
928
+ key, val = m.group(1), m.group(2).strip()
929
+ if val in ('|', '>', '|-', '>-', ''):
930
+ # block scalar: consume the indented run beneath it
931
+ body, i = [], i + 1
932
+ while i < len(lines) and (not lines[i].strip() or lines[i][:1] in (' ', '\t')):
933
+ body.append(lines[i].strip())
934
+ i += 1
935
+ joined = ' '.join(x for x in body if x)
936
+ if joined:
937
+ out[key] = joined
938
+ continue
939
+ out[key] = val.strip('"\'')
940
+ i += 1
941
+ return out
942
+
943
+
784
944
  # ── Tools & Commands Scanner ──────────────────────────────────────────────────
785
945
 
786
946
  class ToolsScanner:
@@ -827,6 +987,22 @@ class ToolsScanner:
827
987
  if (skill_dir / 'SKILL.md').exists():
828
988
  tools.append({'name': f'/{skill_dir.name}', 'command': f'/{skill_dir.name}', 'description': 'Claude skill', 'source': 'skills'})
829
989
 
990
+ # Slash commands — commands/*.md and .claude/commands/*.md
991
+ seen = {t['name'] for t in tools}
992
+ for manifest in SkillParser().parse():
993
+ if manifest['kind'] != 'command':
994
+ continue
995
+ name = f"/{manifest['name']}"
996
+ if name in seen:
997
+ continue
998
+ seen.add(name)
999
+ tools.append({
1000
+ 'name': name,
1001
+ 'command': name,
1002
+ 'description': manifest['description'] or 'slash command',
1003
+ 'source': 'commands',
1004
+ })
1005
+
830
1006
  return tools
831
1007
 
832
1008
 
@@ -958,7 +1134,11 @@ def build_vocabulary_section(vocab: list[dict]) -> str:
958
1134
  notes = v.get('notes', '').replace('|', '\\|')
959
1135
  lines.append(f"| {alias} | {type_} | {location} | {notes} |")
960
1136
  if not vocab:
961
- lines.append("| _(no vocabulary generated yet — add source code to populate)_ | | | |")
1137
+ # Prose, not a table row: a placeholder row parses as a valid entry with
1138
+ # an empty (=neutral) location, which scored an empty vocabulary at 100%
1139
+ # and left grader.py's greenfield branch permanently unreachable.
1140
+ lines.append("")
1141
+ lines.append("_No vocabulary generated yet — add source code to populate._")
962
1142
  return '\n'.join(lines) + '\n'
963
1143
 
964
1144
 
@@ -1260,20 +1440,9 @@ def build_dead_code_section(candidates: list[dict]) -> str:
1260
1440
 
1261
1441
  def build_doc_pointers_section() -> str:
1262
1442
  lines = ["# Section 19 — Documentation Pointers\n\n"]
1263
- docs = []
1264
- doc_dirs = ['docs', 'doc', 'documentation', 'wiki', '.docs']
1265
- doc_exts = {'.md', '.rst', '.txt', '.adoc'}
1266
-
1267
- for doc_dir in doc_dirs:
1268
- d = PROJECT_ROOT / doc_dir
1269
- if d.is_dir():
1270
- for f in sorted(d.rglob('*')):
1271
- if f.is_file() and f.suffix in doc_exts:
1272
- docs.append(str(f.relative_to(PROJECT_ROOT)))
1273
-
1274
- # Root-level docs
1275
- for f in PROJECT_ROOT.glob('*.md'):
1276
- docs.append(str(f.relative_to(PROJECT_ROOT)))
1443
+ # Same walk the checksum watches (DOC_DIRS/DOC_EXTS), so the pointer list and
1444
+ # the watch set cannot drift apart.
1445
+ docs = [str(f.relative_to(PROJECT_ROOT)) for f in collect_doc_files()]
1277
1446
 
1278
1447
  if not docs:
1279
1448
  lines.append("_No documentation files found._\n")
@@ -1297,6 +1466,7 @@ def build_project_map(
1297
1466
  services: list[dict],
1298
1467
  vocab: list[dict],
1299
1468
  section_files: list[tuple[str, Path]],
1469
+ scanned: dict[str, int] | None = None,
1300
1470
  ) -> str:
1301
1471
  now = datetime.now().strftime('%Y-%m-%d %H:%M')
1302
1472
  name = stack.get('name', PROJECT_ROOT.name)
@@ -1315,6 +1485,16 @@ def build_project_map(
1315
1485
  f"| Docker Services | {len(services)} |",
1316
1486
  f"| Vocabulary Entries | {len(vocab)} |",
1317
1487
  f"| Stack | {stack.get('language','?')} / {stack.get('framework','?')} |",
1488
+ "",
1489
+ # Inputs beside outputs. "0 routes" alone cannot be judged: from 47
1490
+ # Python files it means an extractor is broken, from 0 it is correct.
1491
+ # grader.py reads these to tell those two cases apart.
1492
+ "### Source files scanned\n",
1493
+ "| Language | Files |",
1494
+ "|----------|-------|",
1495
+ ] + [
1496
+ f"| {lang} | {count} |" for lang, count in sorted((scanned or {}).items())
1497
+ ] + [
1318
1498
  "",
1319
1499
  "## Section Index\n",
1320
1500
  "| # | Section | Size | When to Read |",
@@ -1392,7 +1572,7 @@ def main() -> None:
1392
1572
 
1393
1573
  # 1. Checksum check
1394
1574
  watched = collect_watched_files()
1395
- checksum = compute_checksum(watched)
1575
+ checksum = compute_checksum(watched, collect_doc_files())
1396
1576
 
1397
1577
  if not args.force and is_unchanged(checksum):
1398
1578
  print("[generate] ✓ No changes detected — skipping regeneration (use --force to override)")
@@ -1434,12 +1614,13 @@ def main() -> None:
1434
1614
  env_entries = EnvParser().parse()
1435
1615
  migrations = MigrationParser().parse()
1436
1616
  features = FrontendScanner().scan()
1617
+ skills = SkillParser().parse()
1437
1618
  tools = ToolsScanner().scan()
1438
1619
  auth_info = AuthScanner().scan()
1439
1620
  proxy_rules = ReverseProxyScanner().scan()
1440
1621
 
1441
1622
  print("[generate] Building vocabulary...")
1442
- vocab = VocabularyBuilder().build(routes, models, schemas, features, stack)
1623
+ vocab = VocabularyBuilder().build(routes, models, schemas, features, stack, skills)
1443
1624
 
1444
1625
  print("[generate] Tracing import chains...")
1445
1626
  chains = ImportChainTracer().trace(routes)
@@ -1473,7 +1654,11 @@ def main() -> None:
1473
1654
 
1474
1655
  # 6. Write PROJECT_MAP.md
1475
1656
  print("[generate] Writing PROJECT_MAP.md...")
1476
- project_map = build_project_map(stack, routes, models, schemas, features, migrations, services, vocab, section_files)
1657
+ project_map = build_project_map(
1658
+ stack, routes, models, schemas, features, migrations, services, vocab, section_files,
1659
+ scanned={'python': len(py_files), 'typescript/javascript': len(ts_files),
1660
+ 'go': len(go_files)},
1661
+ )
1477
1662
  (MAP_DIR / 'PROJECT_MAP.md').write_text(project_map, encoding='utf-8')
1478
1663
 
1479
1664
  # 7. Update checksums
@@ -173,7 +173,7 @@ def grade_import_chains() -> GradeResult:
173
173
 
174
174
  content = chains_file.read_text(encoding='utf-8', errors='replace')
175
175
 
176
- if '_No import chains traced' in content or '_No' in content:
176
+ if '_No import chains traced' in content:
177
177
  # For greenfield or non-Python: acceptable
178
178
  details = "No chains traced (greenfield or non-Python stack — ok)"
179
179
  return GradeResult('import_chain_validity', WEIGHTS['import_chain_validity'],
@@ -497,6 +497,17 @@ def print_terminal_summary(results: list[GradeResult], total_score: float, passe
497
497
  print(f" {'TOTAL SCORE':<30} {score_color}{total_score:5.1f}%{RESET} → {verdict}")
498
498
  print(f"{CYAN}{'─' * 55}{RESET}\n")
499
499
 
500
+ pop, total_sections = populated_sections()
501
+ if total_sections:
502
+ print(f" {'Sections populated':<30} {pop}/{total_sections}"
503
+ f" (diagnostic — not scored)")
504
+ print()
505
+
506
+ for w in usefulness_warnings():
507
+ print(f"{YELLOW} ⚠ {w}{RESET}")
508
+ if usefulness_warnings():
509
+ print()
510
+
500
511
  all_issues = [(r.display_name, i) for r in results for i in r.issues]
501
512
  if all_issues:
502
513
  print(f"{YELLOW} Issues:{RESET}")
@@ -507,6 +518,71 @@ def print_terminal_summary(results: list[GradeResult], total_score: float, passe
507
518
  print()
508
519
 
509
520
 
521
+ # ── Usefulness diagnostics (reported, never scored) ──────────────────────────
522
+ # The seven graded categories all measure FORM, and generate.py always emits
523
+ # well-formed output — so an empty map and a populated one score identically
524
+ # (both 97.0% before this was added). The information that separates them is
525
+ # not in the map, so it is surfaced as a warning rather than folded into the
526
+ # score: making it scored would fail legitimately sparse repos, which is a
527
+ # worse failure than the one it fixes.
528
+
529
+ def _stat(content: str, label: str) -> int | None:
530
+ m = re.search(rf'^\|\s*{re.escape(label)}\s*\|\s*(\d+)\s*\|', content, re.MULTILINE)
531
+ return int(m.group(1)) if m else None
532
+
533
+
534
+ def usefulness_warnings() -> list[str]:
535
+ map_file = MAP_DIR / 'PROJECT_MAP.md'
536
+ if not map_file.exists():
537
+ return []
538
+ content = map_file.read_text(encoding='utf-8', errors='replace')
539
+ warnings = []
540
+
541
+ # Vocabulary is the product. Zero entries means the map gave the user
542
+ # nothing, whatever the seven form categories say. This is the signal that
543
+ # would have surfaced issue #6 at install time.
544
+ vocab = _stat(content, 'Vocabulary Entries')
545
+ if vocab == 0:
546
+ warnings.append(
547
+ "Vocabulary is empty — the map's primary output produced nothing. "
548
+ "Expected on a repo with no source code; otherwise an extractor "
549
+ "does not understand this project's shape."
550
+ )
551
+
552
+ # Source files present but nothing extracted from them: a parser that does
553
+ # not fit the stack, rather than a repo with nothing to find.
554
+ scanned = 0
555
+ block = re.search(r'### Source files scanned(.*?)(?:\n## |\Z)', content, re.DOTALL)
556
+ if block:
557
+ scanned = sum(int(n) for n in re.findall(r'^\|[^|]+\|\s*(\d+)\s*\|',
558
+ block.group(1), re.MULTILINE))
559
+ extracted = sum(v or 0 for v in (
560
+ _stat(content, 'API Routes'), _stat(content, 'Data Models'),
561
+ _stat(content, 'Schemas/DTOs'), _stat(content, 'Frontend Features')))
562
+ if scanned >= 10 and extracted == 0:
563
+ warnings.append(
564
+ f"Scanned {scanned} source file(s) but extracted no routes, models, "
565
+ f"schemas or features — likely an unsupported framework."
566
+ )
567
+ return warnings
568
+
569
+
570
+ def populated_sections() -> tuple[int, int]:
571
+ """Sections carrying real content. Diagnostic only — 19 is aspirational,
572
+ not a target: a plugin repo can never populate routes or migrations."""
573
+ files = sorted(SECTIONS_DIR.glob('*.md')) if SECTIONS_DIR.exists() else []
574
+ pop = 0
575
+ for f in files:
576
+ body = f.read_text(encoding='utf-8', errors='replace')
577
+ meat = [ln for ln in body.splitlines()
578
+ if ln.strip() and not ln.startswith(('#', '>'))
579
+ and not re.fullmatch(r'\|[\s|:-]*\|', ln.strip())
580
+ and not re.match(r'^_\(?(no|none|not)\b', ln.strip(), re.I)]
581
+ if meat:
582
+ pop += 1
583
+ return pop, len(files)
584
+
585
+
510
586
  # ╔══════════════════════════════════════════════════════════════════════════╗
511
587
  # ║ MAIN ║
512
588
  # ╚══════════════════════════════════════════════════════════════════════════╝