livepilot 1.10.7 → 1.10.8
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.
- package/CHANGELOG.md +126 -0
- package/README.md +11 -9
- package/bin/livepilot.js +146 -28
- package/installer/install.js +117 -11
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +39 -7
- package/mcp_server/atlas/tools.py +56 -15
- package/mcp_server/composer/layer_planner.py +27 -0
- package/mcp_server/composer/prompt_parser.py +15 -6
- package/mcp_server/connection.py +11 -3
- package/mcp_server/corpus/__init__.py +14 -4
- package/mcp_server/m4l_bridge.py +48 -7
- package/mcp_server/runtime/execution_router.py +16 -2
- package/mcp_server/runtime/remote_commands.py +6 -0
- package/mcp_server/sample_engine/models.py +22 -3
- package/mcp_server/semantic_moves/__init__.py +1 -0
- package/mcp_server/semantic_moves/compiler.py +9 -1
- package/mcp_server/semantic_moves/device_creation_compilers.py +47 -0
- package/mcp_server/semantic_moves/mix_compilers.py +170 -0
- package/mcp_server/semantic_moves/mix_moves.py +1 -1
- package/mcp_server/semantic_moves/models.py +5 -0
- package/mcp_server/semantic_moves/tools.py +15 -4
- package/mcp_server/server.py +7 -3
- package/mcp_server/services/singletons.py +68 -0
- package/mcp_server/splice_client/client.py +29 -8
- package/mcp_server/tools/analyzer.py +7 -6
- package/mcp_server/tools/clips.py +1 -1
- package/mcp_server/tools/midi_io.py +10 -0
- package/mcp_server/tools/tracks.py +1 -1
- package/mcp_server/tools/transport.py +1 -1
- package/mcp_server/translation_engine/tools.py +8 -4
- package/package.json +25 -3
- package/remote_script/LivePilot/__init__.py +29 -9
- package/remote_script/LivePilot/arrangement.py +12 -2
- package/remote_script/LivePilot/browser.py +16 -6
- package/remote_script/LivePilot/devices.py +10 -5
- package/remote_script/LivePilot/notes.py +13 -2
- package/remote_script/LivePilot/server.py +51 -13
- package/remote_script/LivePilot/version_detect.py +7 -4
- package/server.json +20 -0
- package/.claude-plugin/marketplace.json +0 -21
- package/.mcp.json.disabled +0 -9
- package/.mcpbignore +0 -60
- package/AGENTS.md +0 -46
- package/BUGS.md +0 -1570
- package/CODE_OF_CONDUCT.md +0 -27
- package/CONTRIBUTING.md +0 -131
- package/SECURITY.md +0 -48
- package/livepilot/.Codex-plugin/plugin.json +0 -8
- package/livepilot/.claude-plugin/plugin.json +0 -8
- package/livepilot/agents/livepilot-producer/AGENT.md +0 -313
- package/livepilot/commands/arrange.md +0 -47
- package/livepilot/commands/beat.md +0 -77
- package/livepilot/commands/evaluate.md +0 -49
- package/livepilot/commands/memory.md +0 -22
- package/livepilot/commands/mix.md +0 -44
- package/livepilot/commands/perform.md +0 -42
- package/livepilot/commands/session.md +0 -13
- package/livepilot/commands/sounddesign.md +0 -43
- package/livepilot/skills/livepilot-arrangement/SKILL.md +0 -155
- package/livepilot/skills/livepilot-composition-engine/SKILL.md +0 -107
- package/livepilot/skills/livepilot-composition-engine/references/form-patterns.md +0 -97
- package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +0 -102
- package/livepilot/skills/livepilot-core/SKILL.md +0 -184
- package/livepilot/skills/livepilot-core/references/ableton-workflow-patterns.md +0 -831
- package/livepilot/skills/livepilot-core/references/automation-atlas.md +0 -272
- package/livepilot/skills/livepilot-core/references/device-atlas/00-index.md +0 -110
- package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +0 -687
- package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +0 -753
- package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +0 -525
- package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +0 -402
- package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +0 -963
- package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +0 -874
- package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +0 -571
- package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +0 -714
- package/livepilot/skills/livepilot-core/references/device-atlas/synths-native.md +0 -953
- package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +0 -34
- package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +0 -204
- package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +0 -173
- package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +0 -211
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +0 -188
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +0 -162
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +0 -229
- package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +0 -243
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +0 -352
- package/livepilot/skills/livepilot-core/references/memory-guide.md +0 -107
- package/livepilot/skills/livepilot-core/references/midi-recipes.md +0 -402
- package/livepilot/skills/livepilot-core/references/mixing-patterns.md +0 -578
- package/livepilot/skills/livepilot-core/references/overview.md +0 -290
- package/livepilot/skills/livepilot-core/references/sample-manipulation.md +0 -724
- package/livepilot/skills/livepilot-core/references/sound-design-deep.md +0 -140
- package/livepilot/skills/livepilot-core/references/sound-design.md +0 -393
- package/livepilot/skills/livepilot-devices/SKILL.md +0 -169
- package/livepilot/skills/livepilot-evaluation/SKILL.md +0 -156
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +0 -118
- package/livepilot/skills/livepilot-evaluation/references/evaluation-contracts.md +0 -121
- package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +0 -110
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +0 -123
- package/livepilot/skills/livepilot-mix-engine/references/mix-critics.md +0 -143
- package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +0 -105
- package/livepilot/skills/livepilot-mixing/SKILL.md +0 -157
- package/livepilot/skills/livepilot-notes/SKILL.md +0 -130
- package/livepilot/skills/livepilot-performance-engine/SKILL.md +0 -122
- package/livepilot/skills/livepilot-performance-engine/references/performance-safety.md +0 -98
- package/livepilot/skills/livepilot-release/SKILL.md +0 -130
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +0 -105
- package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +0 -87
- package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +0 -51
- package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +0 -131
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +0 -168
- package/livepilot/skills/livepilot-sound-design-engine/references/patch-model.md +0 -119
- package/livepilot/skills/livepilot-sound-design-engine/references/sound-design-critics.md +0 -118
- package/livepilot/skills/livepilot-wonder/SKILL.md +0 -79
- package/m4l_device/LivePilot_Analyzer.amxd.pre-presentation-backup +0 -0
- package/m4l_device/LivePilot_Analyzer.maxpat +0 -2705
- package/m4l_device/LivePilot_Analyzer.maxproj +0 -53
- package/manifest.json +0 -91
- package/mcp_server/splice_client/protos/app_pb2.pyi +0 -1153
- package/scripts/generate_tool_catalog.py +0 -106
- package/scripts/sync_metadata.py +0 -349
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Generate tool catalog from live runtime metadata.
|
|
3
|
-
|
|
4
|
-
Produces a markdown tool catalog validated against mcp.list_tools().
|
|
5
|
-
This is the single source of truth — hand-edited catalogs are replaced.
|
|
6
|
-
|
|
7
|
-
Usage: python3 scripts/generate_tool_catalog.py > docs/manual/tool-catalog-generated.md
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import asyncio
|
|
11
|
-
import inspect
|
|
12
|
-
import sys
|
|
13
|
-
from collections import defaultdict
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
17
|
-
sys.path.insert(0, str(ROOT))
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def get_tools() -> list[dict]:
|
|
21
|
-
"""Get all registered tools with metadata."""
|
|
22
|
-
from mcp_server.server import mcp
|
|
23
|
-
|
|
24
|
-
tools_raw = asyncio.run(mcp.list_tools())
|
|
25
|
-
tools = []
|
|
26
|
-
for t in tools_raw:
|
|
27
|
-
# Get the module path to determine domain
|
|
28
|
-
func = t.fn if hasattr(t, "fn") else None
|
|
29
|
-
module = ""
|
|
30
|
-
if func:
|
|
31
|
-
module = func.__module__ if hasattr(func, "__module__") else ""
|
|
32
|
-
|
|
33
|
-
# Get parameter names
|
|
34
|
-
params = []
|
|
35
|
-
if func:
|
|
36
|
-
sig = inspect.signature(func)
|
|
37
|
-
for name, param in sig.parameters.items():
|
|
38
|
-
if name == "ctx":
|
|
39
|
-
continue
|
|
40
|
-
required = param.default is inspect.Parameter.empty
|
|
41
|
-
params.append({"name": name, "required": required})
|
|
42
|
-
|
|
43
|
-
tools.append({
|
|
44
|
-
"name": t.name,
|
|
45
|
-
"description": t.description[:120] if hasattr(t, "description") and t.description else "",
|
|
46
|
-
"module": module,
|
|
47
|
-
"params": params,
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
return tools
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def infer_domain(module: str) -> str:
|
|
54
|
-
"""Infer domain from module path using the same module-layout rule as
|
|
55
|
-
``scripts/sync_metadata.py``:
|
|
56
|
-
- ``mcp_server.<X>.<...>`` → ``<X>``
|
|
57
|
-
- ``mcp_server.tools.<Y>`` → ``<Y>``
|
|
58
|
-
The display name is Title-Cased; underscores become spaces.
|
|
59
|
-
"""
|
|
60
|
-
parts = module.split(".")
|
|
61
|
-
if len(parts) < 2 or parts[0] != "mcp_server":
|
|
62
|
-
return "Other"
|
|
63
|
-
if parts[1] == "tools":
|
|
64
|
-
domain_key = parts[2] if len(parts) > 2 else "other"
|
|
65
|
-
else:
|
|
66
|
-
domain_key = parts[1]
|
|
67
|
-
return domain_key.replace("_", " ").title()
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def main():
|
|
71
|
-
tools = get_tools()
|
|
72
|
-
total = len(tools)
|
|
73
|
-
|
|
74
|
-
# Group by domain
|
|
75
|
-
domains = defaultdict(list)
|
|
76
|
-
for t in tools:
|
|
77
|
-
domain = infer_domain(t["module"])
|
|
78
|
-
domains[domain].append(t)
|
|
79
|
-
|
|
80
|
-
print(f"# LivePilot — Full Tool Catalog (Generated)")
|
|
81
|
-
print()
|
|
82
|
-
print(f"{total} tools across {len(domains)} domains.")
|
|
83
|
-
print()
|
|
84
|
-
print("> Auto-generated from `mcp.list_tools()`. Do not hand-edit.")
|
|
85
|
-
print("> Regenerate: `python3 scripts/generate_tool_catalog.py`")
|
|
86
|
-
print()
|
|
87
|
-
print("---")
|
|
88
|
-
print()
|
|
89
|
-
|
|
90
|
-
for domain in sorted(domains.keys()):
|
|
91
|
-
tool_list = sorted(domains[domain], key=lambda t: t["name"])
|
|
92
|
-
print(f"## {domain} ({len(tool_list)})")
|
|
93
|
-
print()
|
|
94
|
-
print("| Tool | Description |")
|
|
95
|
-
print("|------|-------------|")
|
|
96
|
-
for t in tool_list:
|
|
97
|
-
desc = t["description"].split("\n")[0].strip()
|
|
98
|
-
print(f"| `{t['name']}` | {desc} |")
|
|
99
|
-
print()
|
|
100
|
-
|
|
101
|
-
print(f"---")
|
|
102
|
-
print(f"*Generated from {total} registered tools.*")
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if __name__ == "__main__":
|
|
106
|
-
main()
|
package/scripts/sync_metadata.py
DELETED
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Metadata sync — single source of truth for version, tool count, and domain count.
|
|
3
|
-
|
|
4
|
-
Reads version from package.json, tool count from test_tools_contract.py,
|
|
5
|
-
derives domain count + list from mcp_server source layout, and verifies all
|
|
6
|
-
known locations are in sync.
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python scripts/sync_metadata.py --check # verify, exit 1 if stale
|
|
10
|
-
python scripts/sync_metadata.py --fix # auto-fix stale references
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import json
|
|
14
|
-
import re
|
|
15
|
-
import sys
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
ROOT = Path(__file__).resolve().parents[1]
|
|
19
|
-
TOOLS_ROOT = ROOT / "mcp_server"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_version() -> str:
|
|
23
|
-
"""Read version from package.json (source of truth)."""
|
|
24
|
-
pkg = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
|
|
25
|
-
return pkg["version"]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def get_tool_count() -> int:
|
|
29
|
-
"""Read tool count from test_tools_contract.py assertion."""
|
|
30
|
-
src = (ROOT / "tests" / "test_tools_contract.py").read_text(encoding="utf-8")
|
|
31
|
-
match = re.search(r"assert len\(tools\) == (\d+)", src)
|
|
32
|
-
if match:
|
|
33
|
-
return int(match.group(1))
|
|
34
|
-
raise ValueError("Could not find tool count assertion in test_tools_contract.py")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def get_domains() -> tuple[int, list[str]]:
|
|
38
|
-
"""Derive the set of tool domains from mcp_server source layout.
|
|
39
|
-
|
|
40
|
-
A domain is:
|
|
41
|
-
- the subdirectory name for ``mcp_server/<X>/...`` files containing ``@mcp.tool()``
|
|
42
|
-
- the file stem for ``mcp_server/tools/<Y>.py`` files
|
|
43
|
-
|
|
44
|
-
Returns (count, sorted list of names).
|
|
45
|
-
"""
|
|
46
|
-
domains: set[str] = set()
|
|
47
|
-
for py in TOOLS_ROOT.rglob("*.py"):
|
|
48
|
-
try:
|
|
49
|
-
content = py.read_text(encoding="utf-8")
|
|
50
|
-
except (OSError, UnicodeDecodeError):
|
|
51
|
-
continue
|
|
52
|
-
if "@mcp.tool()" not in content:
|
|
53
|
-
continue
|
|
54
|
-
rel_parts = py.relative_to(TOOLS_ROOT).parts
|
|
55
|
-
if len(rel_parts) < 2:
|
|
56
|
-
# Top-level file (e.g., server.py). No such file currently registers
|
|
57
|
-
# tools; if one does, it would need an explicit domain assignment.
|
|
58
|
-
continue
|
|
59
|
-
if rel_parts[0] == "tools":
|
|
60
|
-
domains.add(py.stem)
|
|
61
|
-
else:
|
|
62
|
-
domains.add(rel_parts[0])
|
|
63
|
-
return len(domains), sorted(domains)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# Files that must contain the version string
|
|
67
|
-
VERSION_FILES = [
|
|
68
|
-
"package.json",
|
|
69
|
-
"server.json",
|
|
70
|
-
"manifest.json",
|
|
71
|
-
"livepilot/.claude-plugin/plugin.json",
|
|
72
|
-
"livepilot/.Codex-plugin/plugin.json",
|
|
73
|
-
".claude-plugin/marketplace.json",
|
|
74
|
-
"mcp_server/__init__.py",
|
|
75
|
-
"remote_script/LivePilot/__init__.py",
|
|
76
|
-
"CLAUDE.md",
|
|
77
|
-
"AGENTS.md",
|
|
78
|
-
"livepilot/skills/livepilot-core/references/overview.md",
|
|
79
|
-
"docs/M4L_BRIDGE.md",
|
|
80
|
-
]
|
|
81
|
-
|
|
82
|
-
# Files that must contain the tool count
|
|
83
|
-
TOOL_COUNT_FILES = [
|
|
84
|
-
"README.md",
|
|
85
|
-
"package.json",
|
|
86
|
-
"server.json",
|
|
87
|
-
"CLAUDE.md",
|
|
88
|
-
"AGENTS.md",
|
|
89
|
-
"CONTRIBUTING.md",
|
|
90
|
-
"livepilot/.claude-plugin/plugin.json",
|
|
91
|
-
"livepilot/.Codex-plugin/plugin.json",
|
|
92
|
-
"livepilot/skills/livepilot-core/SKILL.md",
|
|
93
|
-
"livepilot/skills/livepilot-core/references/overview.md",
|
|
94
|
-
"docs/manual/index.md",
|
|
95
|
-
"docs/manual/tool-reference.md",
|
|
96
|
-
"docs/manual/tool-catalog.md",
|
|
97
|
-
]
|
|
98
|
-
|
|
99
|
-
# Files that must contain the current domain count ("N domains").
|
|
100
|
-
DOMAIN_COUNT_FILES = [
|
|
101
|
-
"README.md",
|
|
102
|
-
"package.json",
|
|
103
|
-
"manifest.json",
|
|
104
|
-
"CLAUDE.md",
|
|
105
|
-
"AGENTS.md",
|
|
106
|
-
"livepilot/.claude-plugin/plugin.json",
|
|
107
|
-
"livepilot/.Codex-plugin/plugin.json",
|
|
108
|
-
".claude-plugin/marketplace.json",
|
|
109
|
-
"livepilot/skills/livepilot-core/SKILL.md",
|
|
110
|
-
"livepilot/skills/livepilot-core/references/overview.md",
|
|
111
|
-
"livepilot/skills/livepilot-release/SKILL.md",
|
|
112
|
-
"docs/manual/index.md",
|
|
113
|
-
"docs/manual/tool-catalog.md",
|
|
114
|
-
"docs/manual/tool-catalog-generated.md",
|
|
115
|
-
"tests/test_tools_contract.py",
|
|
116
|
-
]
|
|
117
|
-
|
|
118
|
-
# Files that enumerate the domain list inline as ``N domains: a, b, c, ...``.
|
|
119
|
-
# Each file's enumeration must match the derived domain set exactly.
|
|
120
|
-
DOMAIN_LIST_FILES = [
|
|
121
|
-
"CLAUDE.md",
|
|
122
|
-
"livepilot/skills/livepilot-release/SKILL.md",
|
|
123
|
-
]
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def check_version(version: str) -> list[str]:
|
|
127
|
-
"""Check all version files for staleness."""
|
|
128
|
-
issues = []
|
|
129
|
-
for rel_path in VERSION_FILES:
|
|
130
|
-
path = ROOT / rel_path
|
|
131
|
-
if not path.exists():
|
|
132
|
-
continue
|
|
133
|
-
content = path.read_text(encoding="utf-8")
|
|
134
|
-
if version not in content:
|
|
135
|
-
# Find what version IS there
|
|
136
|
-
old = re.search(r"1\.\d+\.\d+", content)
|
|
137
|
-
old_ver = old.group(0) if old else "???"
|
|
138
|
-
if old_ver != version:
|
|
139
|
-
issues.append(f" {rel_path}: has {old_ver}, expected {version}")
|
|
140
|
-
return issues
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def check_tool_count(count: int) -> list[str]:
|
|
144
|
-
"""Check all tool count files for staleness."""
|
|
145
|
-
issues = []
|
|
146
|
-
count_str = str(count)
|
|
147
|
-
for rel_path in TOOL_COUNT_FILES:
|
|
148
|
-
path = ROOT / rel_path
|
|
149
|
-
if not path.exists():
|
|
150
|
-
continue
|
|
151
|
-
content = path.read_text(encoding="utf-8")
|
|
152
|
-
# Look for "N tools" pattern
|
|
153
|
-
matches = re.findall(r"(\d+)\s+tools", content)
|
|
154
|
-
for m in matches:
|
|
155
|
-
if m != count_str and int(m) > 250: # ignore subset counts like "210 tools"
|
|
156
|
-
issues.append(f" {rel_path}: has '{m} tools', expected '{count_str} tools'")
|
|
157
|
-
break
|
|
158
|
-
return issues
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def check_domain_count(count: int) -> list[str]:
|
|
162
|
-
"""Check all domain-count files for stale numbers."""
|
|
163
|
-
issues = []
|
|
164
|
-
count_str = str(count)
|
|
165
|
-
for rel_path in DOMAIN_COUNT_FILES:
|
|
166
|
-
path = ROOT / rel_path
|
|
167
|
-
if not path.exists():
|
|
168
|
-
continue
|
|
169
|
-
content = path.read_text(encoding="utf-8")
|
|
170
|
-
matches = re.findall(r"(\d+)\s+domains?\b", content)
|
|
171
|
-
for m in matches:
|
|
172
|
-
# Filter historical CHANGELOG-style subset counts (e.g., "5 domains",
|
|
173
|
-
# "17 domains"). Active claim has always been >= 40.
|
|
174
|
-
if m != count_str and int(m) > 35:
|
|
175
|
-
issues.append(
|
|
176
|
-
f" {rel_path}: has '{m} domains', expected '{count_str} domains'"
|
|
177
|
-
)
|
|
178
|
-
break
|
|
179
|
-
return issues
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def check_domain_list(domains: list[str]) -> list[str]:
|
|
183
|
-
"""Verify each DOMAIN_LIST_FILES file enumerates exactly the derived domain set."""
|
|
184
|
-
issues = []
|
|
185
|
-
domain_set = set(domains)
|
|
186
|
-
for rel_path in DOMAIN_LIST_FILES:
|
|
187
|
-
path = ROOT / rel_path
|
|
188
|
-
if not path.exists():
|
|
189
|
-
continue
|
|
190
|
-
content = path.read_text(encoding="utf-8")
|
|
191
|
-
# Match "<N> domains: a, b, c, ..." up to the first period or newline.
|
|
192
|
-
# Allow trailing markdown bold markers after "domains" (e.g. ``**43 domains**:``).
|
|
193
|
-
match = re.search(r"\d+\s+domains?\**\s*[:\-]\s*([^.\n]+)", content)
|
|
194
|
-
if not match:
|
|
195
|
-
issues.append(
|
|
196
|
-
f" {rel_path}: no 'N domains: ...' inline list found to verify"
|
|
197
|
-
)
|
|
198
|
-
continue
|
|
199
|
-
raw_names = (n.strip() for n in match.group(1).split(","))
|
|
200
|
-
listed = {re.sub(r"[^a-z0-9_]", "", n.lower()) for n in raw_names}
|
|
201
|
-
listed.discard("")
|
|
202
|
-
missing = domain_set - listed
|
|
203
|
-
extra = listed - domain_set
|
|
204
|
-
if missing:
|
|
205
|
-
issues.append(
|
|
206
|
-
f" {rel_path}: inline list missing {len(missing)} domain(s) — {', '.join(sorted(missing))}"
|
|
207
|
-
)
|
|
208
|
-
if extra:
|
|
209
|
-
issues.append(
|
|
210
|
-
f" {rel_path}: inline list has {len(extra)} unknown domain(s) — {', '.join(sorted(extra))}"
|
|
211
|
-
)
|
|
212
|
-
return issues
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def _fix_count(
|
|
216
|
-
count: int, files: list[str], noun: str, threshold: int
|
|
217
|
-
) -> list[str]:
|
|
218
|
-
"""Replace every stale ``<N> <noun>(s)`` in *files* with ``<count> <noun>(s)``.
|
|
219
|
-
|
|
220
|
-
Only substitutes where ``N != count`` and ``N > threshold``; this mirrors the
|
|
221
|
-
filtering in the corresponding ``check_*`` function so historical/subset
|
|
222
|
-
numbers are never rewritten.
|
|
223
|
-
"""
|
|
224
|
-
fixed: list[str] = []
|
|
225
|
-
count_str = str(count)
|
|
226
|
-
pattern = re.compile(rf"(\d+)(\s+{re.escape(noun)}s?)\b")
|
|
227
|
-
for rel_path in files:
|
|
228
|
-
path = ROOT / rel_path
|
|
229
|
-
if not path.exists():
|
|
230
|
-
continue
|
|
231
|
-
content = path.read_text(encoding="utf-8")
|
|
232
|
-
seen_old: list[str] = []
|
|
233
|
-
|
|
234
|
-
def replace(match: "re.Match[str]") -> str:
|
|
235
|
-
old = match.group(1)
|
|
236
|
-
if old != count_str and int(old) > threshold:
|
|
237
|
-
seen_old.append(old)
|
|
238
|
-
return f"{count_str}{match.group(2)}"
|
|
239
|
-
return match.group(0)
|
|
240
|
-
|
|
241
|
-
new_content = pattern.sub(replace, content)
|
|
242
|
-
if seen_old:
|
|
243
|
-
path.write_text(new_content, encoding="utf-8")
|
|
244
|
-
fixed.append(f" {rel_path}: {noun} count {seen_old[0]} → {count_str}")
|
|
245
|
-
return fixed
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def fix_tool_count(count: int) -> list[str]:
|
|
249
|
-
return _fix_count(count, TOOL_COUNT_FILES, "tool", threshold=250)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def fix_domain_count(count: int) -> list[str]:
|
|
253
|
-
return _fix_count(count, DOMAIN_COUNT_FILES, "domain", threshold=35)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def fix_domain_list(domains: list[str]) -> list[str]:
|
|
257
|
-
"""Append missing domain names to each DOMAIN_LIST_FILES inline enumeration.
|
|
258
|
-
|
|
259
|
-
Extra (unknown) entries are never auto-removed — the script only adds, so an
|
|
260
|
-
accidental pattern miss can't silently delete a legitimate entry.
|
|
261
|
-
"""
|
|
262
|
-
fixed: list[str] = []
|
|
263
|
-
pattern = re.compile(r"(\d+\s+domains?\**\s*[:\-]\s*)([^.\n]+)(\.|\n)")
|
|
264
|
-
for rel_path in DOMAIN_LIST_FILES:
|
|
265
|
-
path = ROOT / rel_path
|
|
266
|
-
if not path.exists():
|
|
267
|
-
continue
|
|
268
|
-
content = path.read_text(encoding="utf-8")
|
|
269
|
-
match = pattern.search(content)
|
|
270
|
-
if not match:
|
|
271
|
-
continue
|
|
272
|
-
listed_raw = match.group(2)
|
|
273
|
-
listed = {
|
|
274
|
-
re.sub(r"[^a-z0-9_]", "", n.strip().lower())
|
|
275
|
-
for n in listed_raw.split(",")
|
|
276
|
-
}
|
|
277
|
-
listed.discard("")
|
|
278
|
-
missing = [d for d in domains if d not in listed]
|
|
279
|
-
if not missing:
|
|
280
|
-
continue
|
|
281
|
-
new_list = listed_raw.rstrip() + ", " + ", ".join(missing)
|
|
282
|
-
new_content = content[: match.start(2)] + new_list + content[match.end(2) :]
|
|
283
|
-
path.write_text(new_content, encoding="utf-8")
|
|
284
|
-
fixed.append(
|
|
285
|
-
f" {rel_path}: appended {len(missing)} domain(s) — {', '.join(missing)}"
|
|
286
|
-
)
|
|
287
|
-
return fixed
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def main():
|
|
291
|
-
mode = sys.argv[1] if len(sys.argv) > 1 else "--check"
|
|
292
|
-
|
|
293
|
-
version = get_version()
|
|
294
|
-
tool_count = get_tool_count()
|
|
295
|
-
domain_count, domains = get_domains()
|
|
296
|
-
|
|
297
|
-
print(
|
|
298
|
-
f"Source of truth: version={version}, tools={tool_count}, domains={domain_count}"
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
if mode == "--fix":
|
|
302
|
-
fixed = (
|
|
303
|
-
fix_tool_count(tool_count)
|
|
304
|
-
+ fix_domain_count(domain_count)
|
|
305
|
-
+ fix_domain_list(domains)
|
|
306
|
-
)
|
|
307
|
-
if fixed:
|
|
308
|
-
print(f"\nFixed {len(fixed)} reference(s):")
|
|
309
|
-
for f in fixed:
|
|
310
|
-
print(f)
|
|
311
|
-
else:
|
|
312
|
-
print("\nNothing to fix automatically.")
|
|
313
|
-
|
|
314
|
-
remaining = (
|
|
315
|
-
check_version(version)
|
|
316
|
-
+ check_tool_count(tool_count)
|
|
317
|
-
+ check_domain_count(domain_count)
|
|
318
|
-
+ check_domain_list(domains)
|
|
319
|
-
)
|
|
320
|
-
if remaining:
|
|
321
|
-
print(f"\n{len(remaining)} issue(s) remain (manual fix required):")
|
|
322
|
-
for issue in remaining:
|
|
323
|
-
print(issue)
|
|
324
|
-
print(
|
|
325
|
-
"\nNote: --fix covers tool/domain counts and missing domain list entries. "
|
|
326
|
-
"Version strings and extra list entries must be fixed by hand."
|
|
327
|
-
)
|
|
328
|
-
sys.exit(1)
|
|
329
|
-
print("\nAll metadata in sync.")
|
|
330
|
-
sys.exit(0)
|
|
331
|
-
|
|
332
|
-
# --check mode (default)
|
|
333
|
-
all_issues = (
|
|
334
|
-
check_version(version)
|
|
335
|
-
+ check_tool_count(tool_count)
|
|
336
|
-
+ check_domain_count(domain_count)
|
|
337
|
-
+ check_domain_list(domains)
|
|
338
|
-
)
|
|
339
|
-
if all_issues:
|
|
340
|
-
print(f"\nFound {len(all_issues)} stale reference(s):")
|
|
341
|
-
for issue in all_issues:
|
|
342
|
-
print(issue)
|
|
343
|
-
sys.exit(1)
|
|
344
|
-
print("All metadata in sync.")
|
|
345
|
-
sys.exit(0)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if __name__ == "__main__":
|
|
349
|
-
main()
|