hexaswarm-core 0.1.1__py3-none-any.whl
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.
- hexa_swarm_core/__init__.py +7 -0
- hexa_swarm_core/_version.py +1 -0
- hexa_swarm_core/adapters/__init__.py +12 -0
- hexa_swarm_core/adapters/_node_base.py +66 -0
- hexa_swarm_core/adapters/base.py +70 -0
- hexa_swarm_core/adapters/generic_shell.py +47 -0
- hexa_swarm_core/adapters/go_chi.py +47 -0
- hexa_swarm_core/adapters/kotlin_ktor.py +55 -0
- hexa_swarm_core/adapters/node_express.py +38 -0
- hexa_swarm_core/adapters/node_nest.py +36 -0
- hexa_swarm_core/adapters/node_next.py +41 -0
- hexa_swarm_core/adapters/protocol.py +66 -0
- hexa_swarm_core/adapters/py_celery.py +68 -0
- hexa_swarm_core/adapters/py_click.py +75 -0
- hexa_swarm_core/adapters/py_django.py +101 -0
- hexa_swarm_core/adapters/py_fastapi.py +94 -0
- hexa_swarm_core/adapters/registry.py +58 -0
- hexa_swarm_core/adapters/rust_axum.py +46 -0
- hexa_swarm_core/adapters/swift_vapor.py +42 -0
- hexa_swarm_core/archetypes/__init__.py +15 -0
- hexa_swarm_core/archetypes/definitions.py +261 -0
- hexa_swarm_core/archetypes/protocol.py +54 -0
- hexa_swarm_core/archetypes/registry.py +122 -0
- hexa_swarm_core/assets/__init__.py +21 -0
- hexa_swarm_core/assets/tier_a/.ai-sync/ARCHITECTURE_DECISIONS/0001-adopted-hexa.md +22 -0
- hexa_swarm_core/assets/tier_a/.ai-sync/CODEOWNERS.md +29 -0
- hexa_swarm_core/assets/tier_a/.ai-sync/RULES.md +30 -0
- hexa_swarm_core/assets/tier_a/.ai-sync/SYSTEM_STATE.md +22 -0
- hexa_swarm_core/assets/tier_a/.ai-sync/audit/.gitkeep +0 -0
- hexa_swarm_core/assets/tier_a/.ai-sync/changelog/BE_CURRENT.md +10 -0
- hexa_swarm_core/assets/tier_a/.ai-sync/changelog/FE_CURRENT.md +10 -0
- hexa_swarm_core/assets/tier_a/.ai-sync/contracts/README.md +8 -0
- hexa_swarm_core/assets/tier_a/.ai-sync/events/.gitkeep +0 -0
- hexa_swarm_core/assets/tier_a/.ai-sync/locks/.gitkeep +0 -0
- hexa_swarm_core/assets/tier_a/.ai-sync/plans/.gitkeep +0 -0
- hexa_swarm_core/assets/tier_a/.claude/agents/alpha-data.md +31 -0
- hexa_swarm_core/assets/tier_a/.claude/agents/beta-core.md +34 -0
- hexa_swarm_core/assets/tier_a/.claude/agents/cursor-fe.md +36 -0
- hexa_swarm_core/assets/tier_a/.claude/agents/delta-redteam.md +45 -0
- hexa_swarm_core/assets/tier_a/.claude/agents/epsilon-edge.md +36 -0
- hexa_swarm_core/assets/tier_a/.claude/agents/explorer.md +40 -0
- hexa_swarm_core/assets/tier_a/.claude/agents/gamma-commerce.md +34 -0
- hexa_swarm_core/assets/tier_a/.claude/agents/planner.md +30 -0
- hexa_swarm_core/assets/tier_a/.claude/agents/reviewer.md +48 -0
- hexa_swarm_core/assets/tier_a/.claude/agents/writer.md +37 -0
- hexa_swarm_core/assets/tier_a/.claude/hooks/post_edit_ownership.py +113 -0
- hexa_swarm_core/assets/tier_a/.claude/hooks/post_session_archive.py +62 -0
- hexa_swarm_core/assets/tier_a/.claude/hooks/post_write_validate.py +100 -0
- hexa_swarm_core/assets/tier_a/.claude/hooks/pre_tool_use_guard.py +104 -0
- hexa_swarm_core/assets/tier_a/.claude/hooks/session_start_lock_cleanup.py +43 -0
- hexa_swarm_core/assets/tier_a/.claude/hooks/statusline.sh +32 -0
- hexa_swarm_core/assets/tier_a/.claude/mcp.json +21 -0
- hexa_swarm_core/assets/tier_a/.claude/settings.json +60 -0
- hexa_swarm_core/assets/tier_a/.claude/skills/auto-test/SKILL.md +38 -0
- hexa_swarm_core/assets/tier_a/.claude/skills/contract-sync/SKILL.md +41 -0
- hexa_swarm_core/assets/tier_a/.claude/skills/cost-tracker/SKILL.md +45 -0
- hexa_swarm_core/assets/tier_a/.claude/skills/cross-sync-alert/SKILL.md +32 -0
- hexa_swarm_core/assets/tier_a/.claude/skills/deploy-check/SKILL.md +33 -0
- hexa_swarm_core/assets/tier_a/.claude/skills/fix-issue/SKILL.md +48 -0
- hexa_swarm_core/assets/tier_a/.claude/skills/quality-gate/SKILL.md +49 -0
- hexa_swarm_core/assets/tier_a/.claude/skills/security-audit/SKILL.md +48 -0
- hexa_swarm_core/assets/tier_a/.claude/skills/ship/SKILL.md +33 -0
- hexa_swarm_core/assets/tier_a/.claude/skills/worktree-boot/SKILL.md +44 -0
- hexa_swarm_core/cli/__init__.py +0 -0
- hexa_swarm_core/cli/__main__.py +6 -0
- hexa_swarm_core/cli/adopt.py +125 -0
- hexa_swarm_core/cli/app.py +71 -0
- hexa_swarm_core/cli/archetype.py +31 -0
- hexa_swarm_core/cli/contract.py +57 -0
- hexa_swarm_core/cli/cost.py +53 -0
- hexa_swarm_core/cli/heartbeat.py +29 -0
- hexa_swarm_core/cli/killswitch.py +33 -0
- hexa_swarm_core/cli/lock.py +49 -0
- hexa_swarm_core/cli/quality_gate.py +128 -0
- hexa_swarm_core/cli/session.py +35 -0
- hexa_swarm_core/cli/worktree.py +70 -0
- hexa_swarm_core/exceptions.py +94 -0
- hexa_swarm_core/install.py +124 -0
- hexa_swarm_core/invariants.py +143 -0
- hexa_swarm_core/llm/__init__.py +7 -0
- hexa_swarm_core/llm/base.py +176 -0
- hexa_swarm_core/locks/__init__.py +17 -0
- hexa_swarm_core/locks/file_lock.py +156 -0
- hexa_swarm_core/logging/__init__.py +17 -0
- hexa_swarm_core/logging/config.py +103 -0
- hexa_swarm_core/logging/tracing.py +139 -0
- hexa_swarm_core/mcp/__init__.py +6 -0
- hexa_swarm_core/mcp/openapi_server.py +171 -0
- hexa_swarm_core/orchestrator/__init__.py +13 -0
- hexa_swarm_core/orchestrator/pipeline.py +138 -0
- hexa_swarm_core/orchestrator/stage.py +57 -0
- hexa_swarm_core/profile.py +94 -0
- hexa_swarm_core/providers/__init__.py +7 -0
- hexa_swarm_core/providers/base.py +47 -0
- hexa_swarm_core/safety/__init__.py +11 -0
- hexa_swarm_core/safety/ceiling.py +45 -0
- hexa_swarm_core/safety/killswitch.py +47 -0
- hexa_swarm_core/safety/prompt.py +107 -0
- hexa_swarm_core/session.py +55 -0
- hexa_swarm_core/swarm/__init__.py +26 -0
- hexa_swarm_core/swarm/contract_writer.py +126 -0
- hexa_swarm_core/swarm/heartbeat.py +77 -0
- hexa_swarm_core/swarm/worktree.py +143 -0
- hexa_swarm_core/telemetry/__init__.py +3 -0
- hexa_swarm_core/telemetry/cost.py +104 -0
- hexaswarm_core-0.1.1.dist-info/METADATA +64 -0
- hexaswarm_core-0.1.1.dist-info/RECORD +109 -0
- hexaswarm_core-0.1.1.dist-info/WHEEL +4 -0
- hexaswarm_core-0.1.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from hexa_swarm_core.adapters.base import BaseAdapter
|
|
2
|
+
from hexa_swarm_core.adapters.protocol import InvariantEnforcer, StackAdapter
|
|
3
|
+
from hexa_swarm_core.adapters.registry import ADAPTERS, detect_adapters, get_adapter
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"ADAPTERS",
|
|
7
|
+
"BaseAdapter",
|
|
8
|
+
"InvariantEnforcer",
|
|
9
|
+
"StackAdapter",
|
|
10
|
+
"detect_adapters",
|
|
11
|
+
"get_adapter",
|
|
12
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Shared plumbing for Node-family adapters (next, nest, express)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from hexa_swarm_core.adapters.base import BaseAdapter
|
|
8
|
+
from hexa_swarm_core.adapters.protocol import CommandSpec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NodeAdapterBase(BaseAdapter):
|
|
12
|
+
"""Base for Node/TS adapters - handles pnpm/yarn/npm dispatch uniformly."""
|
|
13
|
+
|
|
14
|
+
language = "typescript"
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def _pm(project_root: Path) -> str:
|
|
18
|
+
if (project_root / "pnpm-lock.yaml").exists():
|
|
19
|
+
return "pnpm"
|
|
20
|
+
if (project_root / "yarn.lock").exists():
|
|
21
|
+
return "yarn"
|
|
22
|
+
return "npm"
|
|
23
|
+
|
|
24
|
+
def _run(self, project_root: Path, script: str) -> list[CommandSpec]:
|
|
25
|
+
pm = self._pm(project_root)
|
|
26
|
+
if pm == "pnpm":
|
|
27
|
+
return [CommandSpec(["pnpm", "run", script], description=f"pnpm run {script}")]
|
|
28
|
+
if pm == "yarn":
|
|
29
|
+
return [CommandSpec(["yarn", script], description=f"yarn {script}")]
|
|
30
|
+
return [CommandSpec(["npm", "run", script, "--if-present"], description=f"npm run {script}")]
|
|
31
|
+
|
|
32
|
+
def lint_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
33
|
+
return self._run(project_root, "lint")
|
|
34
|
+
|
|
35
|
+
def test_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
36
|
+
return self._run(project_root, "test")
|
|
37
|
+
|
|
38
|
+
def build_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
39
|
+
return self._run(project_root, "build")
|
|
40
|
+
|
|
41
|
+
def format_check_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
42
|
+
pm = self._pm(project_root)
|
|
43
|
+
if pm == "pnpm":
|
|
44
|
+
return [CommandSpec(["pnpm", "exec", "prettier", "--check", "."], description="prettier --check")]
|
|
45
|
+
if pm == "yarn":
|
|
46
|
+
return [CommandSpec(["yarn", "prettier", "--check", "."], description="yarn prettier --check")]
|
|
47
|
+
return [CommandSpec(["npx", "--yes", "prettier", "--check", "."], description="npx prettier --check")]
|
|
48
|
+
|
|
49
|
+
def typecheck_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
50
|
+
pm = self._pm(project_root)
|
|
51
|
+
if pm == "pnpm":
|
|
52
|
+
return [CommandSpec(["pnpm", "exec", "tsc", "--noEmit"], description="tsc --noEmit")]
|
|
53
|
+
if pm == "yarn":
|
|
54
|
+
return [CommandSpec(["yarn", "tsc", "--noEmit"], description="yarn tsc --noEmit")]
|
|
55
|
+
return [CommandSpec(["npx", "--yes", "tsc", "--noEmit"], description="npx tsc --noEmit")]
|
|
56
|
+
|
|
57
|
+
def dep_audit_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
58
|
+
pm = self._pm(project_root)
|
|
59
|
+
if pm == "pnpm":
|
|
60
|
+
return [CommandSpec(["pnpm", "audit", "--audit-level=high"], description="pnpm audit")]
|
|
61
|
+
if pm == "yarn":
|
|
62
|
+
return [CommandSpec(["yarn", "npm", "audit", "--severity", "high"], description="yarn npm audit")]
|
|
63
|
+
return [CommandSpec(["npm", "audit", "--audit-level=high"], description="npm audit")]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = ["NodeAdapterBase"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""BaseAdapter — a default skeleton most adapters can subclass."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from hexa_swarm_core.adapters.protocol import CommandSpec, InvariantEnforcer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseAdapter:
|
|
11
|
+
"""Common adapter plumbing.
|
|
12
|
+
|
|
13
|
+
Subclasses override attributes (name, language, detect_files, detect_imports,
|
|
14
|
+
priority) and `_*_cmd()` methods as needed. Default implementations return
|
|
15
|
+
empty lists — "no-op" is a valid adapter stance for a stage it doesn't own.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
name: str = "base"
|
|
19
|
+
language: str = "unknown"
|
|
20
|
+
detect_files: list[str] = []
|
|
21
|
+
detect_imports: list[str] = []
|
|
22
|
+
priority: int = 0
|
|
23
|
+
|
|
24
|
+
# -- detection --------------------------------------------------------
|
|
25
|
+
def detect(self, project_root: Path) -> bool:
|
|
26
|
+
if not self.detect_files:
|
|
27
|
+
return False
|
|
28
|
+
matched: list[Path] = []
|
|
29
|
+
for glob in self.detect_files:
|
|
30
|
+
matched.extend(project_root.glob(glob))
|
|
31
|
+
if not matched:
|
|
32
|
+
return False
|
|
33
|
+
if not self.detect_imports:
|
|
34
|
+
return True
|
|
35
|
+
for path in matched:
|
|
36
|
+
try:
|
|
37
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
38
|
+
except OSError:
|
|
39
|
+
continue
|
|
40
|
+
if any(imp in text for imp in self.detect_imports):
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
# -- commands (override in subclasses) --------------------------------
|
|
45
|
+
def lint_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
def format_check_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
def typecheck_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
def test_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
def build_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
def dep_audit_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
def contract_generator(self, project_root: Path) -> CommandSpec | None: # noqa: ARG002
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
def invariant_enforcers(self, project_root: Path) -> list[InvariantEnforcer]: # noqa: ARG002
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ["BaseAdapter"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""generic-shell adapter — fallback for `hexa adopt` on unknown stacks.
|
|
2
|
+
|
|
3
|
+
Does not presume any linter/test runner. Reads `.hexa/profile.yaml` or a
|
|
4
|
+
`Makefile` for user-provided commands. Safe to use anywhere — just a thin
|
|
5
|
+
command registry that keeps the CLI interface uniform across stacks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from hexa_swarm_core.adapters.base import BaseAdapter
|
|
13
|
+
from hexa_swarm_core.adapters.protocol import CommandSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GenericShellAdapter(BaseAdapter):
|
|
17
|
+
name = "generic-shell"
|
|
18
|
+
language = "shell"
|
|
19
|
+
detect_files = ["Makefile", "justfile", "Taskfile.yml"]
|
|
20
|
+
priority = 5 # very low — only matches when nothing else does
|
|
21
|
+
|
|
22
|
+
def detect(self, project_root: Path) -> bool:
|
|
23
|
+
# Always available as an explicit fallback, but `detect()` only returns
|
|
24
|
+
# True when a Make-like runner exists. Registry still allows forcing
|
|
25
|
+
# this adapter via `--stack generic-shell`.
|
|
26
|
+
return any((project_root / f).exists() for f in self.detect_files)
|
|
27
|
+
|
|
28
|
+
def _make_cmd(self, target: str, description: str) -> list[CommandSpec]:
|
|
29
|
+
return [CommandSpec(["make", target], description=description)]
|
|
30
|
+
|
|
31
|
+
def lint_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
32
|
+
return self._make_cmd("lint", "make lint") if (project_root / "Makefile").exists() else []
|
|
33
|
+
|
|
34
|
+
def format_check_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
35
|
+
return self._make_cmd("fmt", "make fmt") if (project_root / "Makefile").exists() else []
|
|
36
|
+
|
|
37
|
+
def typecheck_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
38
|
+
return self._make_cmd("typecheck", "make typecheck") if (project_root / "Makefile").exists() else []
|
|
39
|
+
|
|
40
|
+
def test_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
41
|
+
return self._make_cmd("test", "make test") if (project_root / "Makefile").exists() else []
|
|
42
|
+
|
|
43
|
+
def build_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
44
|
+
return self._make_cmd("build", "make build") if (project_root / "Makefile").exists() else []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = ["GenericShellAdapter"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""go-chi StackAdapter - Go services using the chi router."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from hexa_swarm_core.adapters.base import BaseAdapter
|
|
9
|
+
from hexa_swarm_core.adapters.protocol import CommandSpec, InvariantEnforcer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GoChiAdapter(BaseAdapter):
|
|
13
|
+
name = "go-chi"
|
|
14
|
+
language = "go"
|
|
15
|
+
detect_files = ["go.mod"]
|
|
16
|
+
detect_imports = ["github.com/go-chi/chi"]
|
|
17
|
+
priority = 65
|
|
18
|
+
|
|
19
|
+
def lint_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
20
|
+
return [CommandSpec(["golangci-lint", "run", "./..."], description="golangci-lint")]
|
|
21
|
+
|
|
22
|
+
def format_check_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
23
|
+
return [CommandSpec(["gofmt", "-l", "."], description="gofmt -l (nonempty = drift)")]
|
|
24
|
+
|
|
25
|
+
def typecheck_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
26
|
+
return [CommandSpec(["go", "build", "-o", os.devnull, "./..."], description="go build (typecheck)")]
|
|
27
|
+
|
|
28
|
+
def test_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
29
|
+
return [CommandSpec(["go", "test", "-race", "-count=1", "./..."], description="go test -race")]
|
|
30
|
+
|
|
31
|
+
def build_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
32
|
+
return [CommandSpec(["go", "build", "./..."], description="go build")]
|
|
33
|
+
|
|
34
|
+
def dep_audit_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
35
|
+
return [CommandSpec(["govulncheck", "./..."], description="govulncheck")]
|
|
36
|
+
|
|
37
|
+
def invariant_enforcers(self, project_root: Path) -> list[InvariantEnforcer]: # noqa: ARG002
|
|
38
|
+
return [
|
|
39
|
+
InvariantEnforcer(
|
|
40
|
+
invariant="S5",
|
|
41
|
+
description="go vet must pass before merge",
|
|
42
|
+
cmd=["go", "vet", "./..."],
|
|
43
|
+
),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = ["GoChiAdapter"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""kotlin-ktor StackAdapter - Kotlin services using Ktor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from hexa_swarm_core.adapters.base import BaseAdapter
|
|
8
|
+
from hexa_swarm_core.adapters.protocol import CommandSpec, InvariantEnforcer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class KotlinKtorAdapter(BaseAdapter):
|
|
12
|
+
name = "kotlin-ktor"
|
|
13
|
+
language = "kotlin"
|
|
14
|
+
detect_files = [
|
|
15
|
+
"build.gradle.kts",
|
|
16
|
+
"build.gradle",
|
|
17
|
+
"settings.gradle.kts",
|
|
18
|
+
]
|
|
19
|
+
detect_imports = ["io.ktor"]
|
|
20
|
+
priority = 65
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def _gradle(project_root: Path) -> str:
|
|
24
|
+
return "./gradlew" if (project_root / "gradlew").exists() else "gradle"
|
|
25
|
+
|
|
26
|
+
def lint_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
27
|
+
g = self._gradle(project_root)
|
|
28
|
+
return [CommandSpec([g, "ktlintCheck"], description="ktlint")]
|
|
29
|
+
|
|
30
|
+
def format_check_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
31
|
+
g = self._gradle(project_root)
|
|
32
|
+
return [CommandSpec([g, "detekt"], description="detekt")]
|
|
33
|
+
|
|
34
|
+
def typecheck_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
35
|
+
g = self._gradle(project_root)
|
|
36
|
+
return [CommandSpec([g, "compileKotlin", "--no-daemon"], description="kotlin compile")]
|
|
37
|
+
|
|
38
|
+
def test_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
39
|
+
g = self._gradle(project_root)
|
|
40
|
+
return [CommandSpec([g, "test", "--no-daemon"], description="gradle test")]
|
|
41
|
+
|
|
42
|
+
def build_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
43
|
+
g = self._gradle(project_root)
|
|
44
|
+
return [CommandSpec([g, "build", "-x", "test", "--no-daemon"], description="gradle build")]
|
|
45
|
+
|
|
46
|
+
def dep_audit_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
47
|
+
g = self._gradle(project_root)
|
|
48
|
+
# Requires the OWASP dependency-check plugin; a no-op when absent.
|
|
49
|
+
return [CommandSpec([g, "dependencyCheckAnalyze", "--no-daemon"], description="owasp dep-check")]
|
|
50
|
+
|
|
51
|
+
def invariant_enforcers(self, project_root: Path) -> list[InvariantEnforcer]: # noqa: ARG002
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = ["KotlinKtorAdapter"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""node-express StackAdapter - Express / Fastify / Koa style Node services."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from hexa_swarm_core.adapters._node_base import NodeAdapterBase
|
|
9
|
+
from hexa_swarm_core.adapters.protocol import InvariantEnforcer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NodeExpressAdapter(NodeAdapterBase):
|
|
13
|
+
name = "node-express"
|
|
14
|
+
detect_files = ["package.json"]
|
|
15
|
+
detect_imports = ["express", "fastify", "koa"]
|
|
16
|
+
priority = 58
|
|
17
|
+
|
|
18
|
+
def detect(self, project_root: Path) -> bool:
|
|
19
|
+
pkg = project_root / "package.json"
|
|
20
|
+
if not pkg.exists():
|
|
21
|
+
return False
|
|
22
|
+
try:
|
|
23
|
+
data = json.loads(pkg.read_text(encoding="utf-8"))
|
|
24
|
+
except (OSError, json.JSONDecodeError):
|
|
25
|
+
return False
|
|
26
|
+
deps = dict(data.get("dependencies") or {})
|
|
27
|
+
deps.update(data.get("devDependencies") or {})
|
|
28
|
+
# `next` and `@nestjs/*` are owned by dedicated adapters - opt out
|
|
29
|
+
# so polyglot repos don't get this adapter attached twice.
|
|
30
|
+
if "next" in deps or any(d.startswith("@nestjs/") for d in deps):
|
|
31
|
+
return False
|
|
32
|
+
return any(imp in deps for imp in self.detect_imports)
|
|
33
|
+
|
|
34
|
+
def invariant_enforcers(self, project_root: Path) -> list[InvariantEnforcer]: # noqa: ARG002
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["NodeExpressAdapter"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""node-nest StackAdapter - NestJS backend services."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from hexa_swarm_core.adapters._node_base import NodeAdapterBase
|
|
9
|
+
from hexa_swarm_core.adapters.protocol import InvariantEnforcer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NodeNestAdapter(NodeAdapterBase):
|
|
13
|
+
name = "node-nest"
|
|
14
|
+
detect_files = ["package.json", "nest-cli.json"]
|
|
15
|
+
detect_imports = ["@nestjs/core", "@nestjs/common"]
|
|
16
|
+
priority = 68
|
|
17
|
+
|
|
18
|
+
def detect(self, project_root: Path) -> bool:
|
|
19
|
+
if (project_root / "nest-cli.json").exists():
|
|
20
|
+
return True
|
|
21
|
+
pkg = project_root / "package.json"
|
|
22
|
+
if not pkg.exists():
|
|
23
|
+
return False
|
|
24
|
+
try:
|
|
25
|
+
data = json.loads(pkg.read_text(encoding="utf-8"))
|
|
26
|
+
except (OSError, json.JSONDecodeError):
|
|
27
|
+
return False
|
|
28
|
+
deps = dict(data.get("dependencies") or {})
|
|
29
|
+
deps.update(data.get("devDependencies") or {})
|
|
30
|
+
return any(d.startswith("@nestjs/") for d in deps)
|
|
31
|
+
|
|
32
|
+
def invariant_enforcers(self, project_root: Path) -> list[InvariantEnforcer]: # noqa: ARG002
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["NodeNestAdapter"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""node-next StackAdapter - Next.js App Router projects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from hexa_swarm_core.adapters._node_base import NodeAdapterBase
|
|
9
|
+
from hexa_swarm_core.adapters.protocol import InvariantEnforcer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NodeNextAdapter(NodeAdapterBase):
|
|
13
|
+
name = "node-next"
|
|
14
|
+
detect_files = [
|
|
15
|
+
"package.json",
|
|
16
|
+
"next.config.ts",
|
|
17
|
+
"next.config.js",
|
|
18
|
+
"next.config.mjs",
|
|
19
|
+
]
|
|
20
|
+
detect_imports = ["next"]
|
|
21
|
+
priority = 70
|
|
22
|
+
|
|
23
|
+
def detect(self, project_root: Path) -> bool:
|
|
24
|
+
if any((project_root / p).exists() for p in ("next.config.ts", "next.config.js", "next.config.mjs")):
|
|
25
|
+
return True
|
|
26
|
+
pkg = project_root / "package.json"
|
|
27
|
+
if not pkg.exists():
|
|
28
|
+
return False
|
|
29
|
+
try:
|
|
30
|
+
data = json.loads(pkg.read_text(encoding="utf-8"))
|
|
31
|
+
except (OSError, json.JSONDecodeError):
|
|
32
|
+
return False
|
|
33
|
+
deps = dict(data.get("dependencies") or {})
|
|
34
|
+
deps.update(data.get("devDependencies") or {})
|
|
35
|
+
return "next" in deps
|
|
36
|
+
|
|
37
|
+
def invariant_enforcers(self, project_root: Path) -> list[InvariantEnforcer]: # noqa: ARG002
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = ["NodeNextAdapter"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
StackAdapter Protocol — the contract every language/framework adapter fulfils.
|
|
3
|
+
|
|
4
|
+
A polymorphic `quality-gate` / `contract-sync` / `invariants-check` command works
|
|
5
|
+
identically across Python, Node, Go, Rust, etc. — because it dispatches to the
|
|
6
|
+
adapter that matches the current project.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Protocol, runtime_checkable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class CommandSpec:
|
|
18
|
+
"""One shell invocation the adapter wants to run."""
|
|
19
|
+
|
|
20
|
+
cmd: list[str]
|
|
21
|
+
cwd: str | None = None # relative to project root; None = project root
|
|
22
|
+
env: dict[str, str] | None = None
|
|
23
|
+
description: str = ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class InvariantEnforcer:
|
|
28
|
+
"""A check tied to a safety invariant (S1..S6) that an adapter wants to
|
|
29
|
+
enforce on this stack. Runs within the quality-gate or a dedicated
|
|
30
|
+
`invariants` job; failure is merge-blocking.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
invariant: str # e.g. "S1", "S3"
|
|
34
|
+
description: str
|
|
35
|
+
cmd: list[str] # shell command; nonzero exit = violation
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@runtime_checkable
|
|
39
|
+
class StackAdapter(Protocol):
|
|
40
|
+
"""Every adapter implements this. See `BaseAdapter` for a default skeleton."""
|
|
41
|
+
|
|
42
|
+
name: str # e.g. "py-fastapi", "node-next"
|
|
43
|
+
language: str # "python", "typescript", "go", ...
|
|
44
|
+
detect_files: list[str] # file globs whose presence indicates this stack
|
|
45
|
+
detect_imports: list[str] # optional: substrings that must appear in a detect_file
|
|
46
|
+
priority: int # higher wins when multiple adapters match
|
|
47
|
+
|
|
48
|
+
def detect(self, project_root: Path) -> bool:
|
|
49
|
+
"""Return True if this adapter applies to the given project root."""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
def lint_cmd(self, project_root: Path) -> list[CommandSpec]: ...
|
|
53
|
+
def format_check_cmd(self, project_root: Path) -> list[CommandSpec]: ...
|
|
54
|
+
def typecheck_cmd(self, project_root: Path) -> list[CommandSpec]: ...
|
|
55
|
+
def test_cmd(self, project_root: Path) -> list[CommandSpec]: ...
|
|
56
|
+
def build_cmd(self, project_root: Path) -> list[CommandSpec]: ...
|
|
57
|
+
def dep_audit_cmd(self, project_root: Path) -> list[CommandSpec]: ...
|
|
58
|
+
|
|
59
|
+
def contract_generator(self, project_root: Path) -> CommandSpec | None:
|
|
60
|
+
"""Command that regenerates `.ai-sync/contracts/openapi.yaml` (or None)."""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
def invariant_enforcers(self, project_root: Path) -> list[InvariantEnforcer]: ...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = ["CommandSpec", "InvariantEnforcer", "StackAdapter"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""py-celery StackAdapter - Celery workers / Airflow / staged Python pipelines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from hexa_swarm_core.adapters.base import BaseAdapter
|
|
8
|
+
from hexa_swarm_core.adapters.protocol import CommandSpec, InvariantEnforcer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PyCeleryAdapter(BaseAdapter):
|
|
12
|
+
name = "py-celery"
|
|
13
|
+
language = "python"
|
|
14
|
+
detect_files = [
|
|
15
|
+
"pyproject.toml",
|
|
16
|
+
"requirements.txt",
|
|
17
|
+
"src/backend/pyproject.toml",
|
|
18
|
+
"src/backend/requirements.txt",
|
|
19
|
+
"backend/pyproject.toml",
|
|
20
|
+
"backend/requirements.txt",
|
|
21
|
+
]
|
|
22
|
+
# airflow shares the "staged pipeline" archetype shape; also route to this.
|
|
23
|
+
detect_imports = ["celery", "apache-airflow", "prefect"]
|
|
24
|
+
# Below fastapi (70): a FastAPI app with celery workers should still be
|
|
25
|
+
# identified as py-fastapi primarily, with py-celery as an add-on.
|
|
26
|
+
priority = 60
|
|
27
|
+
|
|
28
|
+
def lint_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
29
|
+
return [CommandSpec(["ruff", "check", "."], description="ruff check")]
|
|
30
|
+
|
|
31
|
+
def format_check_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
32
|
+
return [CommandSpec(["ruff", "format", "--check", "."], description="ruff format")]
|
|
33
|
+
|
|
34
|
+
def typecheck_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
35
|
+
return [CommandSpec(["mypy", "."], description="mypy")]
|
|
36
|
+
|
|
37
|
+
def test_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
38
|
+
return [
|
|
39
|
+
CommandSpec(
|
|
40
|
+
["pytest", "-m", "not slow and not e2e", "-q", "--timeout=60"],
|
|
41
|
+
description="pytest (pipeline tests)",
|
|
42
|
+
)
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
def dep_audit_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
46
|
+
if (project_root / "requirements.txt").exists():
|
|
47
|
+
return [
|
|
48
|
+
CommandSpec(["python", "-m", "pip", "install", "--quiet", "pip-audit"], description="ensure pip-audit"),
|
|
49
|
+
CommandSpec(["pip-audit", "-r", "requirements.txt"], description="pip-audit"),
|
|
50
|
+
]
|
|
51
|
+
return [
|
|
52
|
+
CommandSpec(["python", "-m", "pip", "install", "--quiet", "pip-audit"], description="ensure pip-audit"),
|
|
53
|
+
CommandSpec(["pip-audit"], description="pip-audit (env)"),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def invariant_enforcers(self, project_root: Path) -> list[InvariantEnforcer]: # noqa: ARG002
|
|
57
|
+
# S3 (idempotency) is critical for pipeline stages - if a stage retries
|
|
58
|
+
# after a killswitch, it must not double-charge / double-write.
|
|
59
|
+
return [
|
|
60
|
+
InvariantEnforcer(
|
|
61
|
+
invariant="S3",
|
|
62
|
+
description="Pipeline stages marked @idempotent must pass resume-safety checks",
|
|
63
|
+
cmd=["pytest", "-m", "invariants and idempotent", "-q"],
|
|
64
|
+
),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = ["PyCeleryAdapter"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""py-click StackAdapter — Python CLI tools built with Click or Typer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from hexa_swarm_core.adapters.base import BaseAdapter
|
|
8
|
+
from hexa_swarm_core.adapters.protocol import CommandSpec, InvariantEnforcer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PyClickAdapter(BaseAdapter):
|
|
12
|
+
name = "py-click"
|
|
13
|
+
language = "python"
|
|
14
|
+
# CLI-only projects live at the repo root; monorepos may nest backends.
|
|
15
|
+
# Accept both layouts so a fullstack repo with click under src/backend
|
|
16
|
+
# still surfaces the adapter (below fastapi's priority, so it only fires
|
|
17
|
+
# as a fallback or for repos with no fastapi dep).
|
|
18
|
+
detect_files = [
|
|
19
|
+
"pyproject.toml",
|
|
20
|
+
"requirements.txt",
|
|
21
|
+
"setup.py",
|
|
22
|
+
"setup.cfg",
|
|
23
|
+
"src/backend/pyproject.toml",
|
|
24
|
+
"src/backend/requirements.txt",
|
|
25
|
+
"backend/pyproject.toml",
|
|
26
|
+
"backend/requirements.txt",
|
|
27
|
+
]
|
|
28
|
+
detect_imports = ["click", "typer"] # typer is a click superset
|
|
29
|
+
# Lower than py-fastapi so fullstack projects that also import click
|
|
30
|
+
# (e.g. for a `manage.py`) don't steal the adapter slot.
|
|
31
|
+
priority = 55
|
|
32
|
+
|
|
33
|
+
def lint_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
34
|
+
return [CommandSpec(["ruff", "check", "."], description="ruff check")]
|
|
35
|
+
|
|
36
|
+
def format_check_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
37
|
+
return [CommandSpec(["ruff", "format", "--check", "."], description="ruff format")]
|
|
38
|
+
|
|
39
|
+
def typecheck_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
40
|
+
return [CommandSpec(["mypy", "."], description="mypy")]
|
|
41
|
+
|
|
42
|
+
def test_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
43
|
+
return [
|
|
44
|
+
CommandSpec(
|
|
45
|
+
["pytest", "-m", "not slow and not e2e", "-q", "--timeout=30"],
|
|
46
|
+
description="pytest",
|
|
47
|
+
)
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def build_cmd(self, project_root: Path) -> list[CommandSpec]: # noqa: ARG002
|
|
51
|
+
# Optional: produce a wheel so distribution smoke is part of the gate.
|
|
52
|
+
return [CommandSpec(["python", "-m", "pip", "wheel", "--no-deps", "."], description="pip wheel")]
|
|
53
|
+
|
|
54
|
+
def dep_audit_cmd(self, project_root: Path) -> list[CommandSpec]:
|
|
55
|
+
if (project_root / "requirements.txt").exists():
|
|
56
|
+
return [
|
|
57
|
+
CommandSpec(["python", "-m", "pip", "install", "--quiet", "pip-audit"], description="ensure pip-audit"),
|
|
58
|
+
CommandSpec(["pip-audit", "-r", "requirements.txt"], description="pip-audit"),
|
|
59
|
+
]
|
|
60
|
+
return [
|
|
61
|
+
CommandSpec(["python", "-m", "pip", "install", "--quiet", "pip-audit"], description="ensure pip-audit"),
|
|
62
|
+
CommandSpec(["pip-audit"], description="pip-audit (env)"),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
def invariant_enforcers(self, project_root: Path) -> list[InvariantEnforcer]: # noqa: ARG002
|
|
66
|
+
return [
|
|
67
|
+
InvariantEnforcer(
|
|
68
|
+
invariant="S2",
|
|
69
|
+
description="CLI functions marked @deterministic must remain deterministic",
|
|
70
|
+
cmd=["pytest", "-m", "invariants and deterministic", "-q"],
|
|
71
|
+
)
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__all__ = ["PyClickAdapter"]
|