Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-marketplace.json",
"name": "clean-code-toolkit",
"version": "3.2.2",
"version": "3.2.3",
"description": "Clean-code and product-handoff tools for AI-assisted builders.",
"owner": {
"name": "Tarik Moody"
Expand All @@ -10,7 +10,7 @@
{
"name": "clean-code-toolkit",
"description": "Review code, assess product readiness, refactor safely, and prepare a developer handoff.",
"version": "3.2.2",
"version": "3.2.3",
"author": {
"name": "Tarik Moody"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clean-code-toolkit",
"version": "3.2.2",
"version": "3.2.3",
"description": "Practical clean-code, product-readiness, and developer-handoff workflows for AI-assisted projects.",
"author": {
"name": "Tarik Moody"
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 3.2.3

- Bug fix from a real repo: monorepos were read as if only the root `package.json` existed. Workspace members (`apps/*`, `packages/*`, `services/*`) now contribute dependencies, scripts, and platform config (`vercel.json`, `fly.toml`, `netlify.toml`, `wrangler.*`, `render.yaml`, `Procfile`, `convex/schema.*`).
- A Dockerfile inside one workspace member no longer blocks the Vercel inference for a Next.js app in another.
- Found on `tmoody1973/annotated`: before, frameworks `[]` and no adapters; after, Next.js + Convex + Fastify, surfaces Vercel + Fly + Convex, all three adapters, multi-surface rollback finding raised.
- Tests: 27.

## 3.2.2

- Consistency patch, no new features.
Expand Down
79 changes: 54 additions & 25 deletions skills/prod-readiness-coach/scripts/prod_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ class Repo:
def __init__(self, root: Path):
self.root = root.resolve()
self._git_files: Optional[list[str]] = None
self._pkg_cache: Optional[dict] = None

def is_git_repo(self) -> bool:
return (self.root / ".git").exists()
Expand Down Expand Up @@ -251,14 +252,38 @@ def grep(self, pattern: str, paths: Optional[list[str]] = None, flags=re.IGNOREC
hits.append((f, line.strip()[:160]) if not with_lineno else (f, lineno, line))
return hits

def _read_json(self, relpath: str) -> dict:
try:
return json.loads(self.read(relpath)) if (self.root / relpath).exists() else {}
except Exception:
return {}

def workspace_package_files(self) -> list[str]:
"""package.json files of workspace members in a monorepo (pnpm/turbo/npm/yarn workspaces)."""
root = self._read_json("package.json")
is_monorepo = bool(root.get("workspaces")) or self.exists("pnpm-workspace.yaml", "turbo.json", "lerna.json", "nx.json")
if not is_monorepo:
return []
return [f for f in self.find_any(["apps/*/package.json", "packages/*/package.json", "services/*/package.json"])
if "node_modules" not in f]

def package_json(self) -> dict:
for candidate in ("package.json",):
if (self.root / candidate).exists():
try:
return json.loads(self.read(candidate))
except Exception:
return {}
return {}
"""Root package.json, with workspace members' dependencies and scripts merged in.
A monorepo's frameworks live in apps/* and packages/*, not at the root."""
if self._pkg_cache is not None:
return self._pkg_cache
root = self._read_json("package.json")
merged = dict(root)
for key in ("dependencies", "devDependencies", "scripts"):
merged[key] = dict(root.get(key, {}))
for f in self.workspace_package_files():
member = self._read_json(f)
for key in ("dependencies", "devDependencies"):
merged[key] = {**member.get(key, {}), **merged[key]}
for name, cmd in member.get("scripts", {}).items():
merged["scripts"].setdefault(name, cmd)
self._pkg_cache = merged
return merged

def requirements_text(self) -> str:
parts = []
Expand Down Expand Up @@ -372,29 +397,31 @@ def detect_stack_fingerprint(repo: Repo) -> StackFingerprint:
fp.frameworks.append(fw)

# --- Deploy surfaces ------------------------------------------------
if repo.exists("vercel.json") or repo.exists(".vercel") or "vercel-build" in pkg.get("scripts", {}):
fp.add_surface("vercel", repo.exists("vercel.json", ".vercel") or "package.json scripts.vercel-build")
if repo.exists("netlify.toml"):
fp.add_surface("netlify", "netlify.toml")
if repo.exists("fly.toml"):
fp.add_surface("fly", "fly.toml")
if repo.exists("wrangler.toml", "wrangler.jsonc", "wrangler.json"):
fp.add_surface("cloudflare-workers", repo.exists("wrangler.toml", "wrangler.jsonc", "wrangler.json"))
if repo.exists("render.yaml"):
fp.add_surface("render", "render.yaml")
if repo.exists("Procfile"):
fp.add_surface("heroku", "Procfile")
member_vercel = repo.find_any(["apps/*/vercel.json", "packages/*/vercel.json"])
if repo.exists("vercel.json") or repo.exists(".vercel") or member_vercel or "vercel-build" in pkg.get("scripts", {}):
fp.add_surface("vercel", repo.exists("vercel.json", ".vercel") or (member_vercel[0] if member_vercel else "package.json scripts.vercel-build"))
def anywhere(*names: str) -> list[str]:
"""Root or a workspace member (apps/*, packages/*, services/*)."""
pats = list(names) + [f"{d}/*/{n}" for d in ("apps", "packages", "services") for n in names]
return repo.find_any(pats)
for surface, names in (("netlify", ("netlify.toml",)), ("fly", ("fly.toml",)),
("cloudflare-workers", ("wrangler.toml", "wrangler.jsonc", "wrangler.json")),
("render", ("render.yaml",)), ("heroku", ("Procfile",))):
hits = anywhere(*names)
if hits:
fp.add_surface(surface, hits[0])
dockerfiles = repo.find_any(["**/Dockerfile", "Dockerfile"])
compose_files = repo.find_any(["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"])
if dockerfiles or compose_files:
fp.add_surface("docker", ", ".join(dockerfiles[:3] + compose_files[:2]))
if "convex" in fp.frameworks or repo.exists("convex/schema.ts", "convex/schema.js"):
convex_schema = repo.exists("convex/schema.ts", "convex/schema.js") or repo.find_any(["**/convex/schema.ts", "**/convex/schema.js"])
if "convex" in fp.frameworks or convex_schema:
fp.add_surface("convex", "convex/ directory or convex dependency")

# --- Runtimes -------------------------------------------------------
if "convex" in fp.frameworks:
fp.runtimes.append("convex-v8-isolate")
if repo.grep(r'^[\'"]use node[\'"]', paths=repo.find_any(["convex/**/*.ts", "convex/**/*.js"])):
if repo.grep(r'^[\'"]use node[\'"]', paths=repo.find_any(["**/convex/**/*.ts", "**/convex/**/*.js"])):
fp.runtimes.append("convex-node")
if "cloudflare-workers" in fp.deploy_surfaces:
fp.runtimes.append("cloudflare-v8-isolate")
Expand All @@ -414,15 +441,17 @@ def detect_stack_fingerprint(repo: Repo) -> StackFingerprint:
fp.migration_tooling.append("alembic")
if repo.find_any(["db/migrate/*"]):
fp.migration_tooling.append("rails-migrations")
if repo.exists("convex/schema.ts", "convex/schema.js"):
if convex_schema:
fp.migration_tooling.append("convex-schema")

# --- Multi-surface detection ------------------------------------------
# Most Next.js apps on Vercel have no vercel.json. If Next.js is present and
# no other frontend host was detected, assume Vercel (flagged as inferred).
if "nextjs" in fp.frameworks and not any(
sfc in fp.deploy_surfaces for sfc in ("vercel", "netlify", "fly", "cloudflare-workers", "render", "heroku", "docker")):
fp.add_surface("vercel", "inferred: Next.js with no other deploy config (confirm)")
root_docker = bool(repo.exists("Dockerfile", "docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"))
frontend_hosts = ("vercel", "netlify", "cloudflare-workers", "render", "heroku")
if "nextjs" in fp.frameworks and not any(sfc in fp.deploy_surfaces for sfc in frontend_hosts) \
and not root_docker and not (repo.exists("fly.toml")):
fp.add_surface("vercel", "inferred: Next.js with no other frontend host config (confirm)")

independently_deployable = {"vercel", "netlify", "fly", "cloudflare-workers", "render", "heroku", "convex"}
matched_platforms = [s for s in fp.deploy_surfaces if s in independently_deployable]
Expand Down
34 changes: 34 additions & 0 deletions skills/prod-readiness-coach/tests/test_prod_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,40 @@ def test_env_example_required_when_code_reads_env(self):
self.assertEqual(check(r, "sec-1")["status"], "fail")


class Monorepo(unittest.TestCase):
def test_workspace_members_contribute_frameworks_and_adapters(self):
r = audit({
"package.json": json.dumps({"name": "root", "private": True, "devDependencies": {"turbo": "2"}}),
"pnpm-workspace.yaml": "packages:\n - apps/*\n - packages/*\n",
"pnpm-lock.yaml": "",
"apps/web/package.json": json.dumps({"dependencies": {"next": "15", "convex": "1"}, "scripts": {"test": "vitest"}}),
"apps/web/vercel.json": "{}",
"packages/backend/convex/schema.ts": "export default {}",
})
fp = r["stack_fingerprint"]
self.assertIn("nextjs", fp["frameworks"])
self.assertIn("convex", fp["frameworks"])
self.assertIn("vercel", fp["deploy_surfaces"])
self.assertIn("nextjs-vercel", fp["adapters_matched"])
self.assertIn("convex", fp["adapters_matched"])
self.assertTrue(fp["multi_surface"])
self.assertEqual(fp["profile"], "web-app")

def test_worker_dockerfile_in_member_does_not_block_vercel_inference(self):
r = audit({
"package.json": json.dumps({"name": "root", "private": True, "workspaces": ["apps/*"]}),
"package-lock.json": "{}",
"apps/web/package.json": json.dumps({"dependencies": {"next": "15"}}),
"apps/worker/package.json": json.dumps({"dependencies": {"fastify": "5"}}),
"apps/worker/Dockerfile": "FROM node:20",
"apps/worker/fly.toml": "app = 'worker'",
})
fp = r["stack_fingerprint"]
self.assertIn("fly", fp["deploy_surfaces"])
self.assertIn("vercel", fp["deploy_surfaces"])
self.assertTrue(fp["multi_surface"])


class CiDetection(unittest.TestCase):
def test_unittest_step_counts_as_running_tests(self):
wf = "on: push\njobs:\n t:\n runs-on: ubuntu-latest\n steps:\n - run: python -m unittest discover tests\n"
Expand Down
Loading