wip: snapshot das alterações não commitadas (2026-10-20)
Origem: D:/Documents/Github/dev-workflows Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 0
This commit is contained in:
1 parent
c17192c9fe
commit
2ef6f6be10
61 files changed
+7892
No files matched your search
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compares each vendored skill's recorded commit (skills/NOTICE.md) with its upstream HEAD. Exit 1 if any moved.
|
||||
|
||||
python3 qa/check_upstream.py
|
||||
"""
|
||||
import os, re, subprocess, sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def main():
|
||||
notice = open(os.path.join(ROOT, "skills", "NOTICE.md"), encoding="utf-8").read()
|
||||
repos = {}
|
||||
for row in re.findall(r"^\|\s*`[\w-]+`\s*\|(.+)$", notice, re.M):
|
||||
cols = [c.strip() for c in row.split("|")]
|
||||
repo = re.search(r"([\w.-]+/[\w.-]+)", cols[0])
|
||||
sha = re.search(r"`([0-9a-f]{7,40})`", cols[1])
|
||||
if repo and sha:
|
||||
repos[repo.group(1)] = sha.group(1)
|
||||
moved = 0
|
||||
for repo, sha in sorted(repos.items()):
|
||||
p = subprocess.run(["git", "ls-remote", f"https://github.com/{repo}", "HEAD"], capture_output=True, text=True, timeout=60)
|
||||
head = p.stdout.split()[0] if p.returncode == 0 and p.stdout else None
|
||||
if head is None:
|
||||
print(f"? {repo}: não consegui consultar")
|
||||
elif head.startswith(sha):
|
||||
print(f"ok {repo} {sha}")
|
||||
else:
|
||||
moved += 1
|
||||
print(f"-> {repo}: vendorizado {sha}, upstream {head[:7]} (https://github.com/{repo}/compare/{sha}...{head[:7]})")
|
||||
return 1 if moved else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Structural lint for the plugin: frontmatter, sizes, references, links and leftover machine-specific paths.
|
||||
|
||||
Run from anywhere: python3 qa/lint_skills.py (exit 1 on any error)
|
||||
"""
|
||||
import json, os, re, sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
MAX_LINES = 150
|
||||
MAX_DESC = 1024
|
||||
FORBIDDEN = [r"~/\.hermes", r"vision_analyze", r"search_files", r"test_quality\.py", r"wf_quality_harness"]
|
||||
MODELS = {"haiku", "sonnet", "opus", "inherit"}
|
||||
TOOLS = {"Read", "Edit", "Write", "Bash", "Grep", "Glob", "WebFetch", "WebSearch", "NotebookEdit", "Agent", "Skill"}
|
||||
errors = []
|
||||
|
||||
|
||||
def err(path, msg):
|
||||
errors.append(f"{os.path.relpath(path, ROOT)}: {msg}")
|
||||
|
||||
|
||||
def frontmatter(path):
|
||||
text = open(path, encoding="utf-8").read()
|
||||
m = re.match(r"---\n(.*?)\n---\n", text, re.S)
|
||||
if not m:
|
||||
err(path, "sem frontmatter")
|
||||
return {}, text
|
||||
fm = m.group(1)
|
||||
meta = dict(re.findall(r"^([\w-]+):[ \t]*(.*)$", fm, re.M))
|
||||
for key in ("uses", "external", "skills"):
|
||||
lst = re.search(rf"^\s*{key}:\s*\[(.*?)\]", fm, re.M)
|
||||
if lst:
|
||||
meta[key] = [s.strip().strip("'\"") for s in lst.group(1).split(",") if s.strip()]
|
||||
return meta, text
|
||||
|
||||
|
||||
def vendored():
|
||||
rows = open(os.path.join(ROOT, "skills", "NOTICE.md"), encoding="utf-8").read()
|
||||
return set(re.findall(r"^\|\s*`([\w-]+)`\s*\|", rows, re.M))
|
||||
|
||||
|
||||
def check_links(path, text):
|
||||
text = re.sub(r"```.*?```|`[^`\n]*`", "", text, flags=re.S)
|
||||
for target in re.findall(r"\]\(([^)\s]+)\)", text):
|
||||
if re.match(r"(https?:|mailto:|#)", target):
|
||||
continue
|
||||
p = os.path.normpath(os.path.join(os.path.dirname(path), target.split("#")[0]))
|
||||
if not os.path.exists(p):
|
||||
err(path, f"link quebrado: {target}")
|
||||
|
||||
|
||||
def main():
|
||||
skills_dir = os.path.join(ROOT, "skills")
|
||||
names = {d for d in os.listdir(skills_dir) if os.path.isfile(os.path.join(skills_dir, d, "SKILL.md"))}
|
||||
third_party = vendored()
|
||||
catalog = open(os.path.join(ROOT, "docs", "SKILLS.md"), encoding="utf-8").read()
|
||||
|
||||
for name in sorted(names):
|
||||
path = os.path.join(skills_dir, name, "SKILL.md")
|
||||
meta, text = frontmatter(path)
|
||||
if meta.get("name") != name:
|
||||
err(path, f"name '{meta.get('name')}' difere da pasta '{name}'")
|
||||
desc = meta.get("description", "")
|
||||
if not desc:
|
||||
err(path, "description vazia")
|
||||
if name in third_party:
|
||||
continue
|
||||
if len(desc) > MAX_DESC:
|
||||
err(path, f"description com {len(desc)} caracteres (máx {MAX_DESC})")
|
||||
lines = text.count("\n")
|
||||
if lines > MAX_LINES:
|
||||
err(path, f"{lines} linhas (máx {MAX_LINES}); mova detalhes para references/")
|
||||
for used in meta.get("uses", []):
|
||||
if used not in names:
|
||||
err(path, f"uses: '{used}' não existe em skills/")
|
||||
for ext in meta.get("external", []):
|
||||
if f"`{ext}`" not in catalog:
|
||||
err(path, f"external: '{ext}' não está em docs/SKILLS.md")
|
||||
for dirpath, _, files in os.walk(os.path.join(skills_dir, name)):
|
||||
for f in files:
|
||||
if f.endswith(".md"):
|
||||
p = os.path.join(dirpath, f)
|
||||
body = open(p, encoding="utf-8").read()
|
||||
check_links(p, body)
|
||||
for pat in FORBIDDEN:
|
||||
if re.search(pat, body):
|
||||
err(p, f"referência específica de máquina: {pat}")
|
||||
|
||||
agents_dir = os.path.join(ROOT, "agents")
|
||||
for f in sorted(os.listdir(agents_dir)):
|
||||
path = os.path.join(agents_dir, f)
|
||||
meta, text = frontmatter(path)
|
||||
if meta.get("name") != f[:-3]:
|
||||
err(path, f"name '{meta.get('name')}' difere do arquivo")
|
||||
if meta.get("model") not in MODELS:
|
||||
err(path, f"model '{meta.get('model')}' inválido")
|
||||
bad = {t.strip() for t in meta.get("tools", "").split(",")} - TOOLS
|
||||
if bad:
|
||||
err(path, f"tools desconhecidas: {', '.join(sorted(bad))}")
|
||||
for s in meta.get("skills", []):
|
||||
if s not in names:
|
||||
err(path, f"skills: '{s}' não existe em skills/")
|
||||
for pat in FORBIDDEN:
|
||||
if re.search(pat, text):
|
||||
err(path, f"referência específica de máquina: {pat}")
|
||||
|
||||
hooks = json.load(open(os.path.join(ROOT, "hooks", "hooks.json")))
|
||||
for groups in hooks["hooks"].values():
|
||||
for g in groups:
|
||||
for h in g["hooks"]:
|
||||
for script in re.findall(r"\$\{CLAUDE_PLUGIN_ROOT\}/([\w/.-]+)", h["command"]):
|
||||
if not os.path.isfile(os.path.join(ROOT, script)):
|
||||
err(os.path.join(ROOT, "hooks", "hooks.json"), f"script inexistente: {script}")
|
||||
|
||||
for doc in ("README.md", "README.pt-BR.md", "CONTRIBUTING.md", os.path.join("docs", "SKILLS.md")):
|
||||
p = os.path.join(ROOT, doc)
|
||||
if os.path.exists(p):
|
||||
check_links(p, open(p, encoding="utf-8").read())
|
||||
|
||||
for e in errors:
|
||||
print("ERRO " + e)
|
||||
print(f"{len(names)} skills, {len(os.listdir(agents_dir))} agentes, {len(errors)} erro(s)")
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Smoke tests for scripts/quality-gate and the hook scripts, on a throwaway git repo. Stdlib only.
|
||||
|
||||
python3 qa/test_gate.py
|
||||
"""
|
||||
import importlib.util, json, os, subprocess, sys, tempfile, unittest
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
GATE = os.path.join(ROOT, "scripts", "quality-gate")
|
||||
STOP = os.path.join(ROOT, "scripts", "hooks", "stop_gate.py")
|
||||
spec = importlib.util.spec_from_file_location("bash_guard", os.path.join(ROOT, "scripts", "hooks", "bash_guard.py"))
|
||||
bash_guard = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(bash_guard)
|
||||
|
||||
|
||||
def run(cmd, cwd, stdin=None):
|
||||
return subprocess.run(cmd, cwd=cwd, input=stdin, capture_output=True, text=True)
|
||||
|
||||
|
||||
class GateTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.repo = self.tmp.name
|
||||
for c in (["git", "init", "-q"], ["git", "config", "user.email", "t@t"], ["git", "config", "user.name", "t"]):
|
||||
run(c, self.repo)
|
||||
self.write("calc.py", "def add(a, b):\n return a + b\n")
|
||||
self.write("check.py", "from calc import add\nassert add(1, 2) == 3\n")
|
||||
self.write(".dev-workflows.toml", '[commands]\ntest = "python3 check.py"\nlint = "true"\ntypecheck = "true"\n')
|
||||
self.write("package.json", '{"dependencies": {"left-pad": "1"}}')
|
||||
run(["git", "add", "-A"], self.repo)
|
||||
run(["git", "commit", "-qm", "init"], self.repo)
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def write(self, name, text):
|
||||
open(os.path.join(self.repo, name), "w").write(text)
|
||||
|
||||
def gate(self, tier=0):
|
||||
p = run([sys.executable, GATE, "--tier", str(tier)], self.repo)
|
||||
return p.returncode, json.load(open(os.path.join(self.repo, ".dev-workflows", "gate.json")))
|
||||
|
||||
def stop(self):
|
||||
out = run([sys.executable, STOP], self.repo, json.dumps({"cwd": self.repo})).stdout
|
||||
return json.loads(out)["reason"] if out.strip() else None
|
||||
|
||||
def status(self, report, name):
|
||||
return next(c["status"] for c in report["checks"] if c["name"] == name)
|
||||
|
||||
def test_clean_tree_does_not_block(self):
|
||||
self.assertIsNone(self.stop())
|
||||
|
||||
def test_change_without_gate_blocks_then_passing_gate_allows(self):
|
||||
self.write("calc.py", "def add(a, b):\n return a + b\n\n\ndef sub(a, b):\n return a - b\n")
|
||||
self.assertIn("sem gate", self.stop())
|
||||
code, report = self.gate()
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(self.status(report, "test"), "pass")
|
||||
self.assertIsNone(self.stop())
|
||||
|
||||
def test_failing_test_fails_gate_and_blocks(self):
|
||||
self.write("calc.py", "def add(a, b):\n return a - b\n")
|
||||
code, report = self.gate()
|
||||
self.assertEqual((code, report["verdict"]), (1, "fail"))
|
||||
self.assertIn("test", self.stop())
|
||||
|
||||
def test_missing_tool_is_unverified_not_passed(self):
|
||||
self.write(".dev-workflows.toml", '[commands]\ntest = "definitely-not-a-tool-xyz"\n')
|
||||
self.write("calc.py", "def add(a, b):\n return b + a\n")
|
||||
code, report = self.gate()
|
||||
self.assertEqual(self.status(report, "test"), "unverified")
|
||||
self.assertEqual(report["verdict"], "incomplete")
|
||||
|
||||
def test_new_dependency_is_reported(self):
|
||||
self.write("package.json", '{"dependencies": {"left-pad": "1", "is-odd": "3"}}')
|
||||
self.write("calc.py", "def add(a, b):\n return a + b # touched\n")
|
||||
_, report = self.gate()
|
||||
self.assertEqual(report["new_dependencies"], ["npm:is-odd"])
|
||||
|
||||
def test_stale_gate_blocks(self):
|
||||
self.write("calc.py", "def add(a, b):\n return a + b # v2\n")
|
||||
self.gate()
|
||||
os.utime(os.path.join(self.repo, ".dev-workflows", "gate.json"), (1, 1))
|
||||
self.assertIn("mudou depois", self.stop())
|
||||
|
||||
|
||||
class GuardTest(unittest.TestCase):
|
||||
def test_decisions(self):
|
||||
cases = {"git commit --no-verify -m x": "deny", "git push -f origin main": "deny",
|
||||
"git push --force-with-lease": None, "rm -rf /": "deny", "rm -rf node_modules": None,
|
||||
"git reset --hard": "ask", "npm test": None}
|
||||
for cmd, expected in cases.items():
|
||||
self.assertEqual(bash_guard.decide(cmd)[0], expected, cmd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=1)
|
||||
Reference in new issue
Block a user