Complete automation infrastructure and project scaffolding
New files: - .claude/agents/backend-builder.md, frontend-builder.md: Agent definitions - .editorconfig, .env.example, .gitignore, .npmrc, .nvmrc: Project config - .mvn/wrapper/maven-wrapper.properties: Maven wrapper - PHASE_MANIFEST.txt: Phase tracking manifest - docker-compose.yml: PostgreSQL + pgAdmin - package.json, pnpm-workspace.yaml: Frontend workspace - pom.xml: Root Maven configuration - scripts/acceptance/smoke-test.sh: Smoke test script - scripts/run-all.sh: Full pipeline entry point - scripts/tools/export-openapi.sh: OpenAPI export utility Updates: - .claude/settings.json: Agent teams, hooks, sandbox config - CLAUDE.md: Current Phase tracking - All gate scripts: Refined error messages and checks - All hook scripts: Optimized for performance - phase-execute.sh: Enhanced phase execution logic - phase-parallel.sh: Parallel execution support - preflight-check.sh: Comprehensive manifest validation Total: 32 files changed, 1238 insertions(+), 656 deletions(-) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: backend-builder
|
||||
description: "LJWX Platform 后端代码生成器。生成 Java 源码、Flyway 迁移 SQL、MyBatis-Plus 实体/Mapper/Service/Controller。在需要生成后端代码时使用。"
|
||||
model: claude-sonnet-4-6
|
||||
permissionMode: default
|
||||
tools:
|
||||
- Read
|
||||
- Edit
|
||||
- Write
|
||||
- Grep
|
||||
- Glob
|
||||
- Bash
|
||||
disallowedTools:
|
||||
- WebFetch
|
||||
- WebSearch
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
你是 LJWX Platform 的后端构建器。职责是生成高质量、符合所有硬规则的 Java 后端代码。
|
||||
|
||||
## 工作流程
|
||||
|
||||
1. **读取指令上下文**:读取 CLAUDE.md(硬规则)和当前 Phase Brief(scope + 验收条件)
|
||||
2. **读取 spec**:仅读取 Phase Brief "读取清单"中列出的 spec 章节,禁止扫描整个 spec/
|
||||
3. **逐文件生成**:按 scope 顺序逐一输出完整文件,禁止省略(禁止 `// ...省略...`)
|
||||
4. **增量编译验证**:每生成 3-5 个 Java 文件后运行 `mvn clean compile -f pom.xml -q`
|
||||
5. **修复编译错误**:若编译失败,读取错误信息,精确修改对应文件,直到编译通过
|
||||
6. **写入 PHASE_MANIFEST.txt**:按要求格式追加当前 Phase 记录
|
||||
7. **运行 gate**:`bash scripts/gates/gate-all.sh <phase-number>`
|
||||
8. **修复 gate 失败**:若 gate 有 FAIL,逐条修复,重新运行 gate 直到全部 PASS
|
||||
|
||||
## 硬规则(不可违反)
|
||||
|
||||
- **DAG 依赖**:`core ← {security, data} ← web ← app`,禁止反向 import
|
||||
- **审计字段**:所有业务实体(非 Quartz)必须继承含 7 列的 `BaseEntity`
|
||||
- **@PreAuthorize**:每个 `@*Mapping` 方法必须有 `@PreAuthorize("hasAuthority('resource:action')")`,login/refresh 除外
|
||||
- **DTO 无 tenantId**:DTO/Request/Response 禁止含 tenantId 字段
|
||||
- **Service 无 setTenantId**:tenant_id 由 TenantContext(JWT claim)自动注入,Service 层禁止手动设置
|
||||
- **Flyway 无 IF NOT EXISTS**:迁移 SQL 禁止此语句
|
||||
- **POM 版本硬编码**:禁止 `${latest.version}` 占位符
|
||||
- **BCrypt cost=10**:`new BCryptPasswordEncoder(10)`
|
||||
- **输出完整性**:每个新文件必须输出完整内容
|
||||
|
||||
## 代码风格参考
|
||||
|
||||
### Controller
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/users")
|
||||
@RequiredArgsConstructor
|
||||
public class UserController {
|
||||
private final UserAppService userAppService;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('user:read')")
|
||||
public Result<PageResult<UserVO>> list(UserQueryDTO query) {
|
||||
return Result.ok(userAppService.listUsers(query));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service(不含 setTenantId)
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserAppService {
|
||||
private final UserMapper userMapper;
|
||||
|
||||
public PageResult<UserVO> listUsers(UserQueryDTO query) {
|
||||
// TenantLineInterceptor 自动注入 tenant_id,这里不需要手动设置
|
||||
List<UserVO> list = userMapper.selectUserList(query);
|
||||
long total = userMapper.countUsers(query);
|
||||
return new PageResult<>(list, total);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Flyway 迁移 SQL
|
||||
```sql
|
||||
CREATE TABLE sys_user (
|
||||
id BIGINT NOT NULL,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
-- 7 audit columns — required for all business tables
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
created_by BIGINT NOT NULL DEFAULT 0,
|
||||
created_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by BIGINT NOT NULL DEFAULT 0,
|
||||
updated_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted TINYINT NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
```
|
||||
@@ -2,6 +2,7 @@
|
||||
name: code-reviewer
|
||||
description: "Diff-driven code reviewer for LJWX platform. Reviews ONLY files listed in the prompt against Phase scope and hard rules. Read-only access. Outputs structured findings. Use proactively after implementing a phase."
|
||||
model: claude-sonnet-4-6
|
||||
permissionMode: plan
|
||||
tools:
|
||||
- Read
|
||||
- Grep
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
name: frontend-builder
|
||||
description: "LJWX Platform 前端代码生成器。生成 Vue 3 + TypeScript 页面、路由、API 调用、Pinia Store、样式。在需要生成前端代码时使用。"
|
||||
model: claude-sonnet-4-6
|
||||
permissionMode: default
|
||||
tools:
|
||||
- Read
|
||||
- Edit
|
||||
- Write
|
||||
- Grep
|
||||
- Glob
|
||||
- Bash
|
||||
disallowedTools:
|
||||
- WebFetch
|
||||
- WebSearch
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
你是 LJWX Platform 的前端构建器。职责是生成高质量、类型安全的 Vue 3 前端代码。
|
||||
|
||||
## 工作流程
|
||||
|
||||
1. **读取指令上下文**:读取 CLAUDE.md(硬规则)和当前 Phase Brief(scope + 验收条件)
|
||||
2. **读取 spec**:仅读取 Phase Brief 中引用的 spec 章节
|
||||
3. **逐文件生成**:按 scope 顺序逐一输出完整文件
|
||||
4. **类型检查验证**:每生成 3-5 个 TS/Vue 文件后运行 `pnpm run type-check`
|
||||
5. **修复类型错误**:精确修改,直到类型检查通过
|
||||
6. **写入 PHASE_MANIFEST.txt**:按要求格式追加当前 Phase 记录
|
||||
7. **运行 gate**:`bash scripts/gates/gate-all.sh <phase-number>`
|
||||
8. **修复 gate 失败**:直到全部 PASS
|
||||
|
||||
## 硬规则(不可违反)
|
||||
|
||||
- **TypeScript strict**:`tsconfig.json` 必须开启 `"strict": true`,禁止 `any`/`as any`/`<any>`
|
||||
- **依赖版本**:`package.json` 中所有版本只用 `~`(tilde),禁止 `^`(caret)
|
||||
- **环境变量**:只允许 `VITE_APP_BASE_API`,禁止 `VITE_API_BASE_URL` 等其他变量名
|
||||
- **Vue Router v5**:使用 v5 API,禁止 v4 已废弃的 `onBeforeRouteLeave`/`onBeforeRouteUpdate`/`useLink`
|
||||
- **组件语法**:使用 `<script setup lang="ts">` 语法糖
|
||||
- **样式作用域**:使用 `<style scoped lang="scss">`
|
||||
- **Axios 401 处理**:实现静默 token 刷新,拦截 401 自动重试
|
||||
- **输出完整性**:每个文件必须输出完整内容
|
||||
|
||||
## 版本锁定(单一事实来源:CLAUDE.md)
|
||||
|
||||
所有版本从 CLAUDE.md "版本锁定" 表获取,禁止自行指定版本。
|
||||
|
||||
## 代码风格参考
|
||||
|
||||
### Vue 组件
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getUserList } from '@/api/user'
|
||||
import type { UserVO } from '@ljwx/shared'
|
||||
|
||||
const loading = ref(false)
|
||||
const users = ref<UserVO[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getUserList()
|
||||
users.value = res.data.rows
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-table v-loading="loading" :data="users">
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// scoped styles
|
||||
</style>
|
||||
```
|
||||
|
||||
### API 调用
|
||||
```typescript
|
||||
import request from '@/api/request'
|
||||
import type { Result, PageResult } from '@ljwx/shared'
|
||||
import type { UserVO, UserQueryDTO } from '@/types/user'
|
||||
|
||||
export function getUserList(params?: UserQueryDTO): Promise<Result<PageResult<UserVO>>> {
|
||||
return request.get('/api/v1/users', { params })
|
||||
}
|
||||
|
||||
export function createUser(data: UserCreateDTO): Promise<Result<number>> {
|
||||
return request.post('/api/v1/users', data)
|
||||
}
|
||||
```
|
||||
|
||||
### Pinia Store
|
||||
```typescript
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { UserInfo } from '@/types/auth'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const userInfo = ref<UserInfo | null>(null)
|
||||
const token = ref<string>('')
|
||||
|
||||
function setToken(newToken: string) {
|
||||
token.value = newToken
|
||||
}
|
||||
|
||||
return { userInfo, token, setToken }
|
||||
})
|
||||
```
|
||||
|
||||
### Vue Router v5(正确写法)
|
||||
```typescript
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layouts/BasicLayout.vue'),
|
||||
children: []
|
||||
}
|
||||
]
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes
|
||||
})
|
||||
```
|
||||
+63
-19
@@ -1,6 +1,30 @@
|
||||
{
|
||||
"permissions": {
|
||||
"defaultMode": "dontAsk",
|
||||
"deny": [
|
||||
"Bash(git push *)",
|
||||
"Bash(rm -rf *)",
|
||||
"Bash(rm -r *)",
|
||||
"Bash(curl *)",
|
||||
"Bash(wget *)",
|
||||
"Bash(sudo *)",
|
||||
"Bash(docker run *)",
|
||||
"Bash(docker exec *)",
|
||||
"Bash(npx *)",
|
||||
"Bash(eval *)",
|
||||
"Bash(sh -c *)",
|
||||
"WebSearch",
|
||||
"WebFetch",
|
||||
"Read(.env)",
|
||||
"Read(.env.*)",
|
||||
"Read(secrets/**)",
|
||||
"Read(**/*.pem)",
|
||||
"Read(**/*.key)",
|
||||
"Read(~/.ssh/**)",
|
||||
"Edit(.env)",
|
||||
"Edit(.env.*)",
|
||||
"Edit(secrets/**)"
|
||||
],
|
||||
"allow": [
|
||||
"Read",
|
||||
"Edit",
|
||||
@@ -9,7 +33,6 @@
|
||||
"Bash(mvn test -f *)",
|
||||
"Bash(mvn verify -f *)",
|
||||
"Bash(mvn dependency:tree -f *)",
|
||||
"Bash(mvn versions:display-dependency-updates -f *)",
|
||||
"Bash(pnpm install)",
|
||||
"Bash(pnpm install --frozen-lockfile)",
|
||||
"Bash(pnpm run *)",
|
||||
@@ -25,9 +48,7 @@
|
||||
"Bash(bash scripts/gates/gate-contract.sh)",
|
||||
"Bash(bash scripts/gates/gate-nfr.sh)",
|
||||
"Bash(bash scripts/preflight/preflight-check.sh)",
|
||||
"Bash(bash scripts/phase-execute.sh *)",
|
||||
"Bash(bash scripts/phase-bundle.sh *)",
|
||||
"Bash(bash scripts/phase-parallel.sh *)",
|
||||
"Bash(bash scripts/review/diff-review.sh *)",
|
||||
"Bash(cat *)",
|
||||
"Bash(grep *)",
|
||||
"Bash(grep -r *)",
|
||||
@@ -39,6 +60,8 @@
|
||||
"Bash(head *)",
|
||||
"Bash(tail *)",
|
||||
"Bash(diff *)",
|
||||
"Bash(sort *)",
|
||||
"Bash(sed *)",
|
||||
"Bash(git add .)",
|
||||
"Bash(git add -A)",
|
||||
"Bash(git commit -m *)",
|
||||
@@ -63,27 +86,48 @@
|
||||
"Task(backend-builder)",
|
||||
"Task(frontend-builder)",
|
||||
"Task(code-reviewer)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(git push *)",
|
||||
"Bash(rm -rf *)",
|
||||
"Bash(rm -r *)",
|
||||
"Bash(curl *)",
|
||||
"Bash(wget *)",
|
||||
"Bash(sudo *)",
|
||||
"Bash(docker run *)",
|
||||
"Bash(docker exec *)",
|
||||
"Bash(npx *)",
|
||||
"Bash(eval *)",
|
||||
"Bash(sh -c *)",
|
||||
"WebSearch"
|
||||
]
|
||||
},
|
||||
"sandbox": {
|
||||
"enabled": true,
|
||||
"mode": "auto-allow"
|
||||
"mode": "auto-allow",
|
||||
"autoAllowBashIfSandboxed": false
|
||||
},
|
||||
"env": {
|
||||
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
|
||||
},
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash $CLAUDE_PROJECT_DIR/scripts/hooks/pre-edit-guard.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash $CLAUDE_PROJECT_DIR/scripts/hooks/post-edit-check.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash $CLAUDE_PROJECT_DIR/scripts/hooks/stop-gate.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.java]
|
||||
indent_size = 4
|
||||
|
||||
[*.xml]
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
@@ -0,0 +1,12 @@
|
||||
# ===== Backend =====
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=ljwx_platform
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=postgres
|
||||
JWT_SECRET=your-256-bit-secret-key-here-change-in-production
|
||||
FILE_BASE_PATH=./uploads
|
||||
|
||||
# ===== Frontend =====
|
||||
VITE_APP_BASE_API=/api
|
||||
VITE_APP_TITLE=LJWX Platform
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# ── Maven ──
|
||||
target/
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# ── IDE ──
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
*.DS_Store
|
||||
|
||||
# ── Node / pnpm ──
|
||||
node_modules/
|
||||
dist/
|
||||
.pnpm-store/
|
||||
|
||||
# ── Environment ──
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# ── Logs ──
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# ── Test / Coverage ──
|
||||
coverage/
|
||||
*.exec
|
||||
surefire-reports/
|
||||
|
||||
# ── OS ──
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
|
||||
@@ -0,0 +1,3 @@
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
auto-install-peers=true
|
||||
@@ -27,7 +27,7 @@ Phase: 0 (Skeleton) — 尚未开始
|
||||
|
||||
1. **DAG 依赖**: core ← {security, data} ← web ← app。security 和 data 互不依赖。禁止 data import security 的任何类,反之亦然
|
||||
2. **前端 semver**: 所有 dependencies / devDependencies 仅用 `~`(tilde),禁止 `^`(caret)
|
||||
3. **审计字段**: 所有业务表(Quartz 除外)必须含 tenant_id, created_by, created_time, updated_by, updated_time, deleted, version 共 7 列,均 NOT NULL + 有 DEFAULT
|
||||
3. **审计字段 (audit fields)**: 所有业务表(Quartz 除外)必须含 tenant_id, created_by, created_time, updated_by, updated_time, deleted, version 共 7 列,均 NOT NULL + 有 DEFAULT
|
||||
4. **TypeScript**: 禁止 `any`,tsconfig 开启 `strict: true`
|
||||
5. **权限注解**: 每个 Controller 方法必须 `@PreAuthorize`(login / refresh 除外),格式 `hasAuthority('resource:action')`,不使用 ROLE_ 前缀
|
||||
6. **tenant_id**: DTO 中禁止出现,前端禁止传递,后端由 Interceptor 自动注入
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
## PHASE 0
|
||||
Status: PASSED
|
||||
|
||||
pom.xml
|
||||
.mvn/wrapper/maven-wrapper.properties
|
||||
pnpm-workspace.yaml
|
||||
package.json
|
||||
.npmrc
|
||||
.nvmrc
|
||||
docker-compose.yml
|
||||
.env.example
|
||||
.gitignore
|
||||
.editorconfig
|
||||
scripts/gates/gate-all.sh
|
||||
scripts/gates/gate-compile.sh
|
||||
scripts/gates/gate-integration.sh
|
||||
scripts/gates/gate-contract.sh
|
||||
scripts/gates/gate-manifest.sh
|
||||
scripts/gates/gate-nfr.sh
|
||||
scripts/tools/export-openapi.sh
|
||||
scripts/acceptance/smoke-test.sh
|
||||
PHASE_MANIFEST.txt
|
||||
@@ -0,0 +1,22 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16.12-alpine
|
||||
container_name: ljwx-postgres
|
||||
ports:
|
||||
- "${DB_PORT:-5432}:5432"
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME:-ljwx_platform}
|
||||
POSTGRES_USER: ${DB_USERNAME:-postgres}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-postgres} -d ${DB_NAME:-ljwx_platform}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "ljwx-platform-frontend",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.30.1",
|
||||
"engines": {
|
||||
"node": ">=20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev:admin": "pnpm --filter ljwx-platform-admin dev",
|
||||
"dev:mobile": "pnpm --filter ljwx-platform-mobile dev:h5",
|
||||
"dev:screen": "pnpm --filter ljwx-platform-screen dev",
|
||||
"build:shared": "pnpm --filter @ljwx/shared build",
|
||||
"build:admin": "pnpm run build:shared && pnpm --filter ljwx-platform-admin build",
|
||||
"build:mobile": "pnpm run build:shared && pnpm --filter ljwx-platform-mobile build:h5",
|
||||
"build:screen": "pnpm run build:shared && pnpm --filter ljwx-platform-screen build",
|
||||
"build:all": "pnpm run build:shared && pnpm --filter './ljwx-platform-*' build",
|
||||
"type-check": "pnpm --filter './ljwx-platform-*' type-check",
|
||||
"lint": "pnpm --filter './ljwx-platform-*' lint"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
packages:
|
||||
- 'packages/*'
|
||||
- 'ljwx-platform-admin'
|
||||
- 'ljwx-platform-mobile'
|
||||
- 'ljwx-platform-screen'
|
||||
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.11</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.ljwx.platform</groupId>
|
||||
<artifactId>ljwx-platform</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>LJWX Platform</name>
|
||||
<description>Enterprise-grade full-stack scaffold — Java 21 / Spring Boot 3.5</description>
|
||||
|
||||
<modules>
|
||||
<module>ljwx-platform-core</module>
|
||||
<module>ljwx-platform-security</module>
|
||||
<module>ljwx-platform-data</module>
|
||||
<module>ljwx-platform-web</module>
|
||||
<module>ljwx-platform-app</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<mybatis.spring.boot.version>3.0.5</mybatis.spring.boot.version>
|
||||
<springdoc.version>2.8.15</springdoc.version>
|
||||
<testcontainers.version>1.21.4</testcontainers.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- ── Internal modules ── -->
|
||||
<dependency>
|
||||
<groupId>com.ljwx.platform</groupId>
|
||||
<artifactId>ljwx-platform-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ljwx.platform</groupId>
|
||||
<artifactId>ljwx-platform-security</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ljwx.platform</groupId>
|
||||
<artifactId>ljwx-platform-data</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ljwx.platform</groupId>
|
||||
<artifactId>ljwx-platform-web</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ── MyBatis ── -->
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>${mybatis.spring.boot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ── SpringDoc OpenAPI ── -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>${springdoc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ── Testcontainers BOM ── -->
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers-bom</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<parameters>true</parameters>
|
||||
<compilerArgs>
|
||||
<arg>-Xlint:unchecked</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>**/*IT.java</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Smoke test — verifies health endpoint and admin login after deployment.
|
||||
# Usage: bash scripts/acceptance/smoke-test.sh [base-url]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-http://localhost:8080}"
|
||||
|
||||
echo "===== Smoke Test ====="
|
||||
|
||||
# ── Health check ──
|
||||
echo "[1] Health check"
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf "$BASE_URL/actuator/health" > /dev/null 2>&1; then
|
||||
echo " OK: health (attempt $i)"
|
||||
break
|
||||
fi
|
||||
if [[ $i -eq 30 ]]; then
|
||||
echo " FAIL: Health check timed out after 60s"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# ── Login test ──
|
||||
echo "[2] Login test (admin)"
|
||||
RESP=$(curl -sf -X POST "$BASE_URL/api/auth/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"admin","password":"Admin@12345"}' || echo '{}')
|
||||
|
||||
CODE=$(echo "$RESP" | grep -o '"code":[0-9]*' | head -1 | cut -d: -f2 || echo "")
|
||||
if [[ "$CODE" != "200" ]]; then
|
||||
echo " FAIL: Login returned code=${CODE:-unknown}"
|
||||
echo " Response: $RESP"
|
||||
exit 1
|
||||
fi
|
||||
echo " OK: admin login succeeded"
|
||||
|
||||
echo "===== Smoke Test PASSED ====="
|
||||
+32
-19
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# gate-all.sh — 聚合所有 Gate 脚本,按依赖顺序执行
|
||||
# 用法: bash scripts/gates/gate-all.sh [phase-number]
|
||||
# 任何一个 Gate 失败则整体失败,但全部跑完后才报告汇总
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
set -uo pipefail
|
||||
@@ -8,14 +9,7 @@ set -uo pipefail
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
GATES=(
|
||||
"gate-manifest.sh"
|
||||
"gate-rules.sh"
|
||||
"gate-compile.sh"
|
||||
"gate-integration.sh"
|
||||
"gate-contract.sh"
|
||||
"gate-nfr.sh"
|
||||
)
|
||||
PHASE_ARG="${1:-}"
|
||||
|
||||
TOTAL=0
|
||||
PASSED=0
|
||||
@@ -25,30 +19,48 @@ SKIPPED=0
|
||||
|
||||
echo "╔══════════════════════════════════════════════════╗"
|
||||
echo "║ LJWX Gate — Full Check ║"
|
||||
if [[ -n "$PHASE_ARG" ]]; then
|
||||
echo "║ Phase: $PHASE_ARG ║"
|
||||
fi
|
||||
echo "╚══════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
for GATE in "${GATES[@]}"; do
|
||||
GATE_PATH="scripts/gates/$GATE"
|
||||
run_gate() {
|
||||
local gate="$1"
|
||||
shift
|
||||
local gate_path="scripts/gates/$gate"
|
||||
((TOTAL++))
|
||||
|
||||
if [[ ! -f "$GATE_PATH" ]]; then
|
||||
echo " SKIP $GATE (file not found)"
|
||||
if [[ ! -f "$gate_path" ]]; then
|
||||
echo " SKIP $gate (file not found)"
|
||||
((SKIPPED++))
|
||||
continue
|
||||
return
|
||||
fi
|
||||
|
||||
echo "── $GATE ──────────────────────────────────────"
|
||||
if bash "$GATE_PATH"; then
|
||||
echo " PASS $GATE"
|
||||
echo "── $gate ──────────────────────────────────────"
|
||||
if bash "$gate_path" "$@"; then
|
||||
echo " PASS $gate"
|
||||
((PASSED++))
|
||||
else
|
||||
echo " FAIL $GATE"
|
||||
echo " FAIL $gate"
|
||||
((FAILED++))
|
||||
FAILED_NAMES+=("$GATE")
|
||||
FAILED_NAMES+=("$gate")
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
}
|
||||
|
||||
# gate-manifest needs phase arg to check scope files
|
||||
run_gate "gate-manifest.sh" $PHASE_ARG
|
||||
# gate-rules: full-repo scan, no phase arg needed
|
||||
run_gate "gate-rules.sh"
|
||||
# gate-compile: reads phase from CLAUDE.md
|
||||
run_gate "gate-compile.sh"
|
||||
# gate-integration: only runs if Docker available and tests exist
|
||||
run_gate "gate-integration.sh"
|
||||
# gate-contract: only runs if app module exists
|
||||
run_gate "gate-contract.sh"
|
||||
# gate-nfr: NFR checks
|
||||
run_gate "gate-nfr.sh"
|
||||
|
||||
echo "══════════════════════════════════════════════════"
|
||||
echo " Total: $TOTAL | Passed: $PASSED | Failed: $FAILED | Skipped: $SKIPPED"
|
||||
@@ -62,3 +74,4 @@ else
|
||||
echo " GATE RESULT: PASSED"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ cd "$PROJECT_ROOT"
|
||||
ERRORS=0
|
||||
|
||||
# ── Determine which checks to run based on current phase ──
|
||||
CURRENT_PHASE=$(grep -oP 'Current Phase:\s*\K\d+' CLAUDE.md 2>/dev/null || echo "")
|
||||
CURRENT_PHASE=$(sed -n 's/^Phase:[[:space:]]*\([0-9][0-9]*\).*/\1/p' CLAUDE.md 2>/dev/null | head -1 || echo "")
|
||||
PHASE_BRIEF=""
|
||||
TARGET_BACKEND="true"
|
||||
TARGET_FRONTEND="true"
|
||||
|
||||
@@ -98,7 +98,7 @@ fi
|
||||
if [[ -f "spec/03-api.md" ]]; then
|
||||
echo "[Contract] Cross-checking with spec/03-api.md"
|
||||
# Extract expected paths from api spec (lines like "POST /api/v1/auth/login")
|
||||
EXPECTED_PATHS=$(grep -oP '(GET|POST|PUT|DELETE|PATCH)\s+/api/\S+' spec/03-api.md \
|
||||
EXPECTED_PATHS=$(grep -oE '(GET|POST|PUT|DELETE|PATCH)[[:space:]]+/api/[^[:space:]]+' spec/03-api.md \
|
||||
| awk '{print $2}' | sort -u || true)
|
||||
|
||||
if [[ -n "$EXPECTED_PATHS" ]]; then
|
||||
@@ -110,7 +110,7 @@ if [[ -f "spec/03-api.md" ]]; then
|
||||
[[ -z "$expected" ]] && continue
|
||||
# Normalize path parameters: /api/v1/users/{id} → match with regex
|
||||
PATTERN=$(echo "$expected" | sed 's/{[^}]*}/[^\/]*/g')
|
||||
if ! echo "$ACTUAL_PATHS" | grep -qP "^${PATTERN}$"; then
|
||||
if ! echo "$ACTUAL_PATHS" | grep -qE "^${PATTERN}$"; then
|
||||
echo " WARN: Expected path $expected not found in generated spec"
|
||||
((MISSING_COUNT++))
|
||||
fi
|
||||
|
||||
@@ -18,7 +18,7 @@ cd "$PROJECT_ROOT"
|
||||
ERRORS=0
|
||||
|
||||
# ── Quick check: is this a backend phase? ──
|
||||
CURRENT_PHASE=$(grep -oP 'Current Phase:\s*\K\d+' CLAUDE.md 2>/dev/null || echo "")
|
||||
CURRENT_PHASE=$(sed -n 's/^Phase:[[:space:]]*\([0-9][0-9]*\).*/\1/p' CLAUDE.md 2>/dev/null | head -1 || echo "")
|
||||
TARGET_BACKEND="true"
|
||||
if [[ -n "$CURRENT_PHASE" ]]; then
|
||||
PHASE_BRIEF="spec/phase/phase-$(printf '%02d' "$CURRENT_PHASE").md"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# gate-manifest.sh — Phase-local manifest 与 scope 文件检查
|
||||
# 用法: bash scripts/gates/gate-manifest.sh [phase-number]
|
||||
#
|
||||
# 检查项:
|
||||
# 1. PHASE_MANIFEST.txt 存在且非空
|
||||
# 2. 当前 Phase 的 section 存在
|
||||
# 3. Phase Brief 中 scope 列出的具体文件全部存在
|
||||
# 4. Phase Brief 文件本身存在且有 YAML front-matter
|
||||
# 1. Phase Brief 存在且有 YAML front-matter
|
||||
# 2. Phase Brief 中 scope 列出的具体文件全部存在
|
||||
# 3. PHASE_MANIFEST.txt 有对应 Phase section(warn only)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
set -euo pipefail
|
||||
|
||||
@@ -15,10 +15,15 @@ cd "$PROJECT_ROOT"
|
||||
|
||||
ERRORS=0
|
||||
|
||||
# ── Determine current phase ──
|
||||
CURRENT_PHASE=$(grep -oP 'Current Phase:\s*\K\d+' CLAUDE.md 2>/dev/null || echo "")
|
||||
# ── Determine current phase: arg > CLAUDE.md ──
|
||||
if [[ -n "${1:-}" ]]; then
|
||||
CURRENT_PHASE="$1"
|
||||
else
|
||||
CURRENT_PHASE=$(sed -n 's/^Phase:[[:space:]]*\([0-9][0-9]*\).*/\1/p' CLAUDE.md 2>/dev/null | head -1 || echo "")
|
||||
fi
|
||||
|
||||
if [[ -z "$CURRENT_PHASE" ]]; then
|
||||
echo " WARN: Cannot determine Current Phase from CLAUDE.md — skipping manifest check"
|
||||
echo " WARN: Cannot determine current Phase — skipping manifest check"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -46,18 +51,15 @@ else
|
||||
done
|
||||
fi
|
||||
|
||||
# ── Check 2: PHASE_MANIFEST.txt exists and has current phase section ──
|
||||
# ── Check 2: PHASE_MANIFEST.txt — warn only (written AFTER gates pass) ──
|
||||
if [[ ! -f "PHASE_MANIFEST.txt" ]]; then
|
||||
echo " FAIL: PHASE_MANIFEST.txt not found"
|
||||
((ERRORS++))
|
||||
echo " WARN: PHASE_MANIFEST.txt not found (will be written after commit)"
|
||||
elif [[ ! -s "PHASE_MANIFEST.txt" ]]; then
|
||||
echo " FAIL: PHASE_MANIFEST.txt is empty"
|
||||
((ERRORS++))
|
||||
echo " WARN: PHASE_MANIFEST.txt is empty"
|
||||
else
|
||||
MANIFEST_MARKER="## PHASE $CURRENT_PHASE"
|
||||
if ! grep -q "$MANIFEST_MARKER" PHASE_MANIFEST.txt; then
|
||||
echo " FAIL: PHASE_MANIFEST.txt missing section: $MANIFEST_MARKER"
|
||||
((ERRORS++))
|
||||
echo " WARN: PHASE_MANIFEST.txt missing section: $MANIFEST_MARKER (will be added)"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
+14
-14
@@ -27,7 +27,7 @@ warn() { echo " WARNING $1"; ((WARNINGS++)); }
|
||||
|
||||
# ── Helper: grep across all yml/yaml files ──
|
||||
find_in_config() {
|
||||
grep -rPn "$1" --include='*.yml' --include='*.yaml' --include='*.properties' . 2>/dev/null \
|
||||
grep -rEn "$1" --include='*.yml' --include='*.yaml' --include='*.properties' . 2>/dev/null \
|
||||
| grep -v 'node_modules' | grep -v 'target/' || true
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ fi
|
||||
echo "[N02] Caffeine cache TTL: 10 min"
|
||||
CACHE_CFG=$(find_in_config 'caffeine|cache.*spec|cache.*expire')
|
||||
if [[ -n "$CACHE_CFG" ]]; then
|
||||
if ! echo "$CACHE_CFG" | grep -qP '(expireAfterWrite|expire-after-write)\s*[=:]\s*10m|600'; then
|
||||
if ! echo "$CACHE_CFG" | grep -qE '(expireAfterWrite|expire-after-write)[[:space:]]*[=:][[:space:]]*(10m|600)'; then
|
||||
warn "N02 cache-ttl — caffeine config found but TTL may not be 10min"
|
||||
else
|
||||
echo " OK: Caffeine TTL=10min configured"
|
||||
@@ -60,10 +60,10 @@ fi
|
||||
echo "[N03] JWT configuration"
|
||||
JWT_CFG=$(find_in_config 'jwt|access.*token.*expir|refresh.*token.*expir')
|
||||
if [[ -n "$JWT_CFG" ]]; then
|
||||
if ! echo "$JWT_CFG" | grep -qiP '(30|1800)'; then
|
||||
if ! echo "$JWT_CFG" | grep -qiE '(30|1800)'; then
|
||||
warn "N03 jwt — access token expiry may not be 30min"
|
||||
fi
|
||||
if ! echo "$JWT_CFG" | grep -qiP '(7d|604800|10080)'; then
|
||||
if ! echo "$JWT_CFG" | grep -qiE '(7d|604800|10080)'; then
|
||||
warn "N03 jwt — refresh token expiry may not be 7 days"
|
||||
fi
|
||||
fi
|
||||
@@ -76,28 +76,28 @@ if [[ -z "$HS256" ]]; then
|
||||
fi
|
||||
|
||||
echo "[N04] Async log thread pool"
|
||||
ASYNC_CFG=$(grep -rPn 'core.?pool.?size|max.?pool.?size|queue.?capacity' --include='*.java' --include='*.yml' . 2>/dev/null \
|
||||
ASYNC_CFG=$(grep -rEn 'core.?pool.?size|max.?pool.?size|queue.?capacity' --include='*.java' --include='*.yml' . 2>/dev/null \
|
||||
| grep -v 'node_modules' | grep -vi 'quartz' || true)
|
||||
if [[ -n "$ASYNC_CFG" ]]; then
|
||||
echo " OK: Async pool configuration found"
|
||||
# Detailed check
|
||||
if ! echo "$ASYNC_CFG" | grep -qP '(core|corePoolSize)\s*[=:(]\s*2'; then
|
||||
if ! echo "$ASYNC_CFG" | grep -qE '(core|corePoolSize)[[:space:]]*[=:(][[:space:]]*2'; then
|
||||
warn "N04 async-pool — core pool size may not be 2"
|
||||
fi
|
||||
if ! echo "$ASYNC_CFG" | grep -qP '(max|maxPoolSize|maximum)\s*[=:(]\s*4'; then
|
||||
if ! echo "$ASYNC_CFG" | grep -qE '(max|maxPoolSize|maximum)[[:space:]]*[=:(][[:space:]]*4'; then
|
||||
warn "N04 async-pool — max pool size may not be 4"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[N05] Operation log truncation: 4096 bytes"
|
||||
TRUNC=$(grep -rPn '4096|TRUNCATE|truncat' --include='*.java' . 2>/dev/null \
|
||||
TRUNC=$(grep -rEn '4096|TRUNCATE|truncat' --include='*.java' . 2>/dev/null \
|
||||
| grep -v 'node_modules' | grep -i 'log\|oper' || true)
|
||||
if [[ -n "$TRUNC" ]]; then
|
||||
echo " OK: Log truncation reference found"
|
||||
fi
|
||||
|
||||
echo "[N06] Log field masking"
|
||||
MASK=$(grep -rPn 'mask|sensitive|password.*\*|token.*\*|secret.*\*' --include='*.java' . 2>/dev/null \
|
||||
MASK=$(grep -rEn 'mask|sensitive|password.*\*|token.*\*|secret.*\*' --include='*.java' . 2>/dev/null \
|
||||
| grep -v 'node_modules' || true)
|
||||
if [[ -n "$MASK" ]]; then
|
||||
echo " OK: Field masking references found"
|
||||
@@ -116,14 +116,14 @@ fi
|
||||
echo "[N08] Toolchain version locks"
|
||||
# Check Java version
|
||||
if [[ -f "pom.xml" ]]; then
|
||||
JAVA_VER=$(grep -oP '<java.version>\K[^<]+' pom.xml 2>/dev/null || true)
|
||||
JAVA_VER=$(grep '<java.version>' pom.xml 2>/dev/null | sed 's/.*<java.version>//;s|</java.version>.*||' | tr -d '[:space:]')
|
||||
if [[ -n "$JAVA_VER" && "$JAVA_VER" != "21" ]]; then
|
||||
fail "N08 java-version — pom.xml java.version=$JAVA_VER, expected 21"
|
||||
fi
|
||||
fi
|
||||
# Check Node version
|
||||
if [[ -f ".nvmrc" ]]; then
|
||||
NODE_VER=$(cat .nvmrc | tr -d '[:space:]')
|
||||
NODE_VER=$(tr -d '[:space:]' < .nvmrc)
|
||||
if [[ "$NODE_VER" != "22.22.0" ]]; then
|
||||
fail "N08 node-version — .nvmrc=$NODE_VER, expected 22.22.0"
|
||||
else
|
||||
@@ -132,9 +132,9 @@ if [[ -f ".nvmrc" ]]; then
|
||||
fi
|
||||
# Check pnpm version
|
||||
if [[ -f "package.json" ]]; then
|
||||
PNPM_VER=$(grep -oP '"packageManager"\s*:\s*"pnpm@\K[^"]+' package.json 2>/dev/null || true)
|
||||
PNPM_VER=$(grep '"packageManager"' package.json 2>/dev/null | sed 's/.*pnpm@//;s/".*//' | tr -d '[:space:]')
|
||||
if [[ -n "$PNPM_VER" && "$PNPM_VER" != "10.30.1" ]]; then
|
||||
fail "N08 pnpm-version — packageManager pnpm @$PNPM_VER, expected 10.30.1"
|
||||
fail "N08 pnpm-version — packageManager pnpm@$PNPM_VER, expected 10.30.1"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -151,7 +151,7 @@ fi
|
||||
|
||||
echo "[N10] Docker image tag locked"
|
||||
if [[ -f "docker-compose.yml" ]]; then
|
||||
PG_IMAGE=$(grep -oP 'image:\s*\Kpostgres:\S+' docker-compose.yml 2>/dev/null || true)
|
||||
PG_IMAGE=$(grep 'image:.*postgres:' docker-compose.yml 2>/dev/null | sed 's/.*image:[[:space:]]*//' | tr -d '[:space:]')
|
||||
if [[ -n "$PG_IMAGE" ]]; then
|
||||
if [[ "$PG_IMAGE" != "postgres:16.12-alpine" ]]; then
|
||||
fail "N10 docker-tag — postgres image=$PG_IMAGE, expected postgres:16.12-alpine"
|
||||
|
||||
+24
-24
@@ -40,7 +40,8 @@ fi
|
||||
|
||||
echo "[R02] No TypeScript 'any' type"
|
||||
# Match ': any', 'as any', '<any>' but exclude comments and node_modules
|
||||
ANY_HITS=$(grep -rPn '(?<!//.*)(\:\s*any\b|as\s+any\b|<any>)' --include='*.ts' --include='*.vue' . 2>/dev/null \
|
||||
# Use grep -En (ERE) for macOS BSD grep compatibility
|
||||
ANY_HITS=$(grep -rEn '(:\s*any[^a-zA-Z]|as any[^a-zA-Z]|<any>)' --include='*.ts' --include='*.vue' . 2>/dev/null \
|
||||
| grep -v 'node_modules' \
|
||||
| grep -v '\.d\.ts:' \
|
||||
| grep -v '// eslint-disable' \
|
||||
@@ -52,8 +53,8 @@ if [[ -n "$ANY_HITS" ]]; then
|
||||
fi
|
||||
|
||||
echo "[R03] Env var must be VITE_APP_BASE_API only"
|
||||
BAD_ENV=$(grep -rPn 'VITE_API_BASE_URL|VITE_BASE_API|VITE_APP_API[^_]' \
|
||||
--include='*.ts' --include='*.vue' --include='.env*' . 2>/dev/null \
|
||||
BAD_ENV=$(grep -rEn 'VITE_API_BASE_URL|VITE_BASE_API' \
|
||||
--include='*.ts' --include='*.vue' --include='.env' --include='.env.*' . 2>/dev/null \
|
||||
| grep -v 'node_modules' || true)
|
||||
if [[ -n "$BAD_ENV" ]]; then
|
||||
while IFS= read -r hit; do
|
||||
@@ -62,7 +63,7 @@ if [[ -n "$BAD_ENV" ]]; then
|
||||
fi
|
||||
|
||||
echo "[R04] Flyway: no IF NOT EXISTS"
|
||||
IFNE_HITS=$(grep -rPni 'IF\s+NOT\s+EXISTS' --include='*.sql' . 2>/dev/null | grep -v 'node_modules' || true)
|
||||
IFNE_HITS=$(grep -rEni 'IF[[:space:]]+NOT[[:space:]]+EXISTS' --include='*.sql' . 2>/dev/null | grep -v 'node_modules' || true)
|
||||
if [[ -n "$IFNE_HITS" ]]; then
|
||||
while IFS= read -r hit; do
|
||||
fail "R04 flyway-no-ifne — $hit"
|
||||
@@ -73,7 +74,7 @@ echo "[R05] Audit columns in business tables"
|
||||
SQL_FILES=$(find . -name '*.sql' -path '*/migration/*' ! -path '*/node_modules/*' 2>/dev/null || true)
|
||||
AUDIT_COLS=(tenant_id created_by created_time updated_by updated_time deleted version)
|
||||
for sql_file in $SQL_FILES; do
|
||||
# Extract CREATE TABLE blocks
|
||||
# Extract CREATE TABLE names using ERE (macOS compatible)
|
||||
while IFS= read -r table_name; do
|
||||
[[ -z "$table_name" ]] && continue
|
||||
# Skip Quartz tables
|
||||
@@ -84,15 +85,16 @@ for sql_file in $SQL_FILES; do
|
||||
# Get the table definition block
|
||||
TABLE_BLOCK=$(sed -n "/CREATE TABLE.*${table_name}/I,/);/p" "$sql_file")
|
||||
for col in "${AUDIT_COLS[@]}"; do
|
||||
if ! echo "$TABLE_BLOCK" | grep -qi "$col"; then
|
||||
if [[ -z "$(echo "$TABLE_BLOCK" | grep -i "$col")" ]]; then
|
||||
fail "R05 audit-col — $sql_file table $table_name missing column: $col"
|
||||
fi
|
||||
done
|
||||
done < <(grep -oPI 'CREATE\s+TABLE\s+\K\S+' "$sql_file" 2>/dev/null || true)
|
||||
done < <(grep -oEi 'CREATE[[:space:]]+TABLE[[:space:]]+([`"]?)([A-Za-z_][A-Za-z0-9_]*)' "$sql_file" 2>/dev/null \
|
||||
| sed -E 's/CREATE[[:space:]]+TABLE[[:space:]]+[`"]*//i' || true)
|
||||
done
|
||||
|
||||
echo "[R06] DTO must not expose tenant_id"
|
||||
DTO_HITS=$(grep -rPn '(private|protected|public)\s+\S+\s+tenantId' \
|
||||
DTO_HITS=$(grep -rEn '(private|protected|public)[[:space:]]+[^[:space:]]+[[:space:]]+tenantId' \
|
||||
--include='*DTO.java' --include='*Dto.java' \
|
||||
--include='*Request.java' --include='*Response.java' . 2>/dev/null \
|
||||
| grep -v 'node_modules' || true)
|
||||
@@ -101,8 +103,8 @@ if [[ -n "$DTO_HITS" ]]; then
|
||||
fail "R06 dto-no-tenant — $hit"
|
||||
done <<< "$DTO_HITS"
|
||||
fi
|
||||
# Also check record-style DTOs
|
||||
DTO_RECORD_HITS=$(grep -rPn 'tenantId' \
|
||||
# Also check record-style DTOs / constructor params
|
||||
DTO_RECORD_HITS=$(grep -rEn 'tenantId' \
|
||||
--include='*DTO.java' --include='*Dto.java' \
|
||||
--include='*Request.java' --include='*Response.java' . 2>/dev/null \
|
||||
| grep -v 'node_modules' \
|
||||
@@ -110,8 +112,8 @@ DTO_RECORD_HITS=$(grep -rPn 'tenantId' \
|
||||
| grep -v '//' || true)
|
||||
if [[ -n "$DTO_RECORD_HITS" ]]; then
|
||||
while IFS= read -r hit; do
|
||||
# Avoid double-counting the field declarations already caught above
|
||||
if ! echo "$hit" | grep -qP '(private|protected|public)\s+\S+\s+tenantId'; then
|
||||
# Avoid double-counting field declarations already caught above
|
||||
if [[ -z "$(echo "$hit" | grep -E '(private|protected|public)[[:space:]]+[^[:space:]]+[[:space:]]+tenantId')" ]]; then
|
||||
fail "R06 dto-no-tenant (record/param) — $hit"
|
||||
fi
|
||||
done <<< "$DTO_RECORD_HITS"
|
||||
@@ -120,12 +122,12 @@ fi
|
||||
echo "[R07] Controller @PreAuthorize"
|
||||
CONTROLLER_FILES=$(find . -name '*Controller.java' ! -path '*/node_modules/*' 2>/dev/null || true)
|
||||
for ctrl in $CONTROLLER_FILES; do
|
||||
MAPPING_LINES=$(grep -n ' @(Get|Post|Put|Delete|Patch)Mapping' "$ctrl" 2>/dev/null | cut -d: -f1 || true)
|
||||
MAPPING_LINES=$(grep -En '@(Get|Post|Put|Delete|Patch)Mapping' "$ctrl" 2>/dev/null | cut -d: -f1 || true)
|
||||
for LINE_NUM in $MAPPING_LINES; do
|
||||
START=$((LINE_NUM - 5))
|
||||
[[ $START -lt 1 ]] && START=1
|
||||
CONTEXT=$(sed -n "${START},${LINE_NUM}p" "$ctrl")
|
||||
if ! echo "$CONTEXT" | grep -q ' @PreAuthorize'; then
|
||||
if [[ -z "$(echo "$CONTEXT" | grep '@PreAuthorize')" ]]; then
|
||||
fail "R07 preauthorize — $ctrl:$LINE_NUM @*Mapping without @PreAuthorize"
|
||||
fi
|
||||
done
|
||||
@@ -140,10 +142,8 @@ if [[ -n "$POM_HITS" ]]; then
|
||||
fi
|
||||
|
||||
echo "[R09] POM: no SNAPSHOT in release dependencies"
|
||||
# Only check <dependency> version tags, not the project's own version
|
||||
SNAP_HITS=$(grep -rPn '<version>.*SNAPSHOT.*</version>' --include='pom.xml' . 2>/dev/null \
|
||||
| grep -v '<version>\${' \
|
||||
| grep -v '<!-- project version -->' || true)
|
||||
SNAP_HITS=$(grep -rEn '<version>[^<]*SNAPSHOT[^<]*</version>' --include='pom.xml' . 2>/dev/null \
|
||||
| grep -v 'project version' || true)
|
||||
if [[ -n "$SNAP_HITS" ]]; then
|
||||
while IFS= read -r hit; do
|
||||
warn "R09 no-snapshot — $hit"
|
||||
@@ -190,7 +190,7 @@ V4_PATTERNS=(
|
||||
'RouterLink.*:to.*v-slot'
|
||||
)
|
||||
for pattern in "${V4_PATTERNS[@]}"; do
|
||||
V4_HITS=$(grep -rPn "$pattern" --include='*.vue' --include='*.ts' . 2>/dev/null | grep -v 'node_modules' || true)
|
||||
V4_HITS=$(grep -rEn "$pattern" --include='*.vue' --include='*.ts' . 2>/dev/null | grep -v 'node_modules' || true)
|
||||
if [[ -n "$V4_HITS" ]]; then
|
||||
while IFS= read -r hit; do
|
||||
warn "R11 vue-router-v5 — possible v4 API usage: $hit"
|
||||
@@ -199,16 +199,16 @@ for pattern in "${V4_PATTERNS[@]}"; do
|
||||
done
|
||||
|
||||
echo "[R12] BCrypt cost factor"
|
||||
BCRYPT_HITS=$(grep -rPn 'BCryptPasswordEncoder\s*\(' --include='*.java' . 2>/dev/null || true)
|
||||
BCRYPT_HITS=$(grep -rEn 'BCryptPasswordEncoder[[:space:]]*\(' --include='*.java' . 2>/dev/null || true)
|
||||
if [[ -n "$BCRYPT_HITS" ]]; then
|
||||
while IFS= read -r hit; do
|
||||
if ! echo "$hit" | grep -qP 'BCryptPasswordEncoder\s*\(\s*10\s*\)'; then
|
||||
# Could be default constructor (cost=10) or explicit non-10
|
||||
if echo "$hit" | grep -qP 'BCryptPasswordEncoder\s*\(\s*\d'; then
|
||||
# If explicit numeric arg and NOT 10, fail
|
||||
if [[ -n "$(echo "$hit" | grep -E 'BCryptPasswordEncoder[[:space:]]*\([[:space:]]*[0-9]')" ]]; then
|
||||
if [[ -z "$(echo "$hit" | grep -E 'BCryptPasswordEncoder[[:space:]]*\([[:space:]]*10[[:space:]]*\)')" ]]; then
|
||||
fail "R12 bcrypt-cost — $hit — must use cost factor 10"
|
||||
fi
|
||||
# Default constructor is acceptable (defaults to 10)
|
||||
fi
|
||||
# Default constructor BCryptPasswordEncoder() is acceptable (defaults to 10)
|
||||
done <<< "$BCRYPT_HITS"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,240 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# post-edit-check.sh — PostToolUse Hook for Edit|Write
|
||||
#
|
||||
# 在 Claude 成功写入/修改文件之后触发。
|
||||
# 读取磁盘上的实际文件内容进行规则检查(不依赖 tool_input 内容字段)。
|
||||
#
|
||||
# 检查规则:
|
||||
# P01. package.json 无 caret (^)
|
||||
# P02. DTO/Request/Response 不暴露 tenantId
|
||||
# P03. TypeScript 无 'any' 类型
|
||||
# P04. Controller 方法有 @PreAuthorize
|
||||
# P05. SQL 迁移无 IF NOT EXISTS
|
||||
# P06. 前端只用 VITE_APP_BASE_API
|
||||
# P07. POM 无 ${latest.version} 占位符
|
||||
# P08. 业务表 SQL 有审计列(轻量检查)
|
||||
# P09. Java import 不违反 DAG
|
||||
# P10. Vue Router v5 API(无已废弃 v4 模式)
|
||||
#
|
||||
# 通信协议:
|
||||
# exit 0 + 无输出 = 通过
|
||||
# exit 0 + JSON stdout = {"decision":"block","reason":"..."} 反馈给 Claude 修正
|
||||
# (PostToolUse 无法撤销已写入的内容,但 block 会让 Claude 立即修复)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# PostToolUse hook — reads the ACTUAL file on disk after Edit/Write completes.
|
||||
# All content checks operate on the real file, not on tool_input JSON fields.
|
||||
# Outputs a block decision JSON if issues are found.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)
|
||||
[[ -z "$FILE_PATH" ]] && exit 0
|
||||
[[ -f "$FILE_PATH" ]] || exit 0
|
||||
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
||||
|
||||
# 拿不到路径或文件不存在,放行
|
||||
if [[ -z "$FILE_PATH" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
FILE_PATH="${FILE_PATH#./}"
|
||||
if [[ ! -f "$FILE_PATH" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 收集所有问题
|
||||
ISSUES=""
|
||||
|
||||
add_issue() {
|
||||
if [[ -n "$ISSUES" ]]; then
|
||||
ISSUES+=" | "
|
||||
fi
|
||||
ISSUES+="$1"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# P01: package.json 无 caret (^)
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# Rule no-caret: package.json must not use ^ versions
|
||||
if [[ "$FILE_PATH" == *"package.json"* ]]; then
|
||||
CARET_LINES=$(grep -n '"\^' "$FILE_PATH" 2>/dev/null || true)
|
||||
if [[ -n "$CARET_LINES" ]]; then
|
||||
FIRST_LINE=$(echo "$CARET_LINES" | head -1)
|
||||
add_issue "[P01] $FILE_PATH has caret (^) version — must use tilde (~). First occurrence: $FIRST_LINE"
|
||||
if grep -q '"^' "$FILE_PATH"; then
|
||||
HITS=$(grep -n '"^' "$FILE_PATH" | head -3 | tr '\n' '; ')
|
||||
ISSUES="${ISSUES}[no-caret] $FILE_PATH: caret (^) found — use tilde (~). Lines: ${HITS}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# P02: DTO/Request/Response 不暴露 tenantId
|
||||
# 覆盖 class 字段声明、record 参数、方法参数
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ "$FILE_PATH" =~ (DTO|Dto|Request|Response|Req|Resp|Vo|VO)\.java$ ]]; then
|
||||
# 排除 import 行和注释行
|
||||
TENANT_HITS=$(grep -n 'tenantId\|tenant_id' "$FILE_PATH" 2>/dev/null \
|
||||
# Rule dto-no-tenant-id: DTO must not expose tenantId
|
||||
if [[ "$FILE_PATH" =~ (DTO|Dto|Request|Response)\.java$ ]]; then
|
||||
if grep -qiE '(private|protected|public)[[:space:]]+[^[:space:]]+[[:space:]]+tenantId' "$FILE_PATH"; then
|
||||
LINE=$(grep -niE '(private|protected|public)[[:space:]]+[^[:space:]]+[[:space:]]+tenantId' "$FILE_PATH" | head -1)
|
||||
ISSUES="${ISSUES}[dto-no-tenant-id] $FILE_PATH: DTO exposes tenantId ($LINE). Remove it. "
|
||||
fi
|
||||
fi
|
||||
|
||||
# Rule no-any: TypeScript must not use 'any' type
|
||||
if [[ "$FILE_PATH" =~ \.(ts|vue)$ ]]; then
|
||||
ANY_LINE=$(grep -En '(:[[:space:]]*any[^a-zA-Z_]|as any[^a-zA-Z_]|<any>)' "$FILE_PATH" \
|
||||
| grep -v '^\s*//' \
|
||||
| grep -v '^.*import ' || true)
|
||||
if [[ -n "$TENANT_HITS" ]]; then
|
||||
FIRST_HIT=$(echo "$TENANT_HITS" | head -1)
|
||||
add_issue "[P02] $FILE_PATH exposes tenant_id — DTOs must not contain tenantId. Line: $FIRST_HIT"
|
||||
| grep -v '//.*:\s*any' \
|
||||
| head -1 || true)
|
||||
if [[ -n "$ANY_LINE" ]]; then
|
||||
ISSUES="${ISSUES}[no-any] $FILE_PATH: 'any' type at: $ANY_LINE — use proper type. "
|
||||
fi
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# P03: TypeScript 无 'any' 类型
|
||||
# 匹配 ': any', 'as any', '<any>', 排除注释和类型声明文件
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ "$FILE_PATH" =~ \.(ts|vue)$ && ! "$FILE_PATH" =~ \.d\.ts$ ]]; then
|
||||
# 搜索 any 用法,排除注释行
|
||||
ANY_LINES=$(grep -Pn '(?<!//.*)(\:\s*any\b|as\s+any\b|<any>)' "$FILE_PATH" 2>/dev/null \
|
||||
| grep -v '^\s*//' \
|
||||
| grep -v '// eslint-disable' \
|
||||
| grep -v '// @ts-' || true)
|
||||
if [[ -n "$ANY_LINES" ]]; then
|
||||
FIRST_ANY=$(echo "$ANY_LINES" | head -1)
|
||||
COUNT=$(echo "$ANY_LINES" | wc -l)
|
||||
add_issue "[P03] $FILE_PATH has TypeScript 'any' type ($COUNT occurrences). First: $FIRST_ANY. Use proper types."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# P04: Controller 方法有 @PreAuthorize
|
||||
# 在 @*Mapping 注解上方 5 行内必须有 @PreAuthorize
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ "$FILE_PATH" =~ Controller\.java$ ]]; then
|
||||
MAPPING_LINES=$(grep -n ' @(Get|Post|Put|Delete|Patch)Mapping' "$FILE_PATH" 2>/dev/null \
|
||||
| cut -d: -f1 || true)
|
||||
MISSING_AUTH=""
|
||||
for LINE_NUM in $MAPPING_LINES; do
|
||||
[[ -z "$LINE_NUM" ]] && continue
|
||||
START=$((LINE_NUM - 5))
|
||||
[[ $START -lt 1 ]] && START=1
|
||||
CONTEXT=$(sed -n "${START},${LINE_NUM}p" "$FILE_PATH")
|
||||
if ! echo "$CONTEXT" | grep -q ' @PreAuthorize'; then
|
||||
if [[ -n "$MISSING_AUTH" ]]; then
|
||||
MISSING_AUTH+=", "
|
||||
fi
|
||||
MISSING_AUTH+="line $LINE_NUM"
|
||||
fi
|
||||
done
|
||||
if [[ -n "$MISSING_AUTH" ]]; then
|
||||
add_issue "[P04] $FILE_PATH has @*Mapping without @PreAuthorize at: $MISSING_AUTH. Every endpoint must have @PreAuthorize(\"hasAuthority('resource:action')\")."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# P05: SQL 迁移无 IF NOT EXISTS
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# Rule no-if-not-exists: Flyway SQL must not use IF NOT EXISTS
|
||||
if [[ "$FILE_PATH" =~ \.sql$ ]]; then
|
||||
IFNE_LINES=$(grep -in 'IF NOT EXISTS' "$FILE_PATH" 2>/dev/null || true)
|
||||
if [[ -n "$IFNE_LINES" ]]; then
|
||||
FIRST_IFNE=$(echo "$IFNE_LINES" | head -1)
|
||||
add_issue "[P05] $FILE_PATH contains IF NOT EXISTS — Flyway migrations must not use this. Line: $FIRST_IFNE"
|
||||
if grep -qi 'IF NOT EXISTS' "$FILE_PATH"; then
|
||||
LINE=$(grep -ni 'IF NOT EXISTS' "$FILE_PATH" | head -1)
|
||||
ISSUES="${ISSUES}[no-if-not-exists] $FILE_PATH: IF NOT EXISTS found ($LINE). "
|
||||
fi
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# P06: 前端只用 VITE_APP_BASE_API
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ "$FILE_PATH" =~ \.(ts|vue)$ || "$FILE_PATH" =~ \.env ]]; then
|
||||
BAD_ENV=$(grep -Pn 'VITE_API_BASE_URL|VITE_BASE_API|VITE_APP_API[^_]' "$FILE_PATH" 2>/dev/null || true)
|
||||
if [[ -n "$BAD_ENV" ]]; then
|
||||
FIRST_BAD=$(echo "$BAD_ENV" | head -1)
|
||||
add_issue "[P06] $FILE_PATH uses wrong env var name — must be VITE_APP_BASE_API only. Line: $FIRST_BAD"
|
||||
# Rule wrong-env-var: must use VITE_APP_BASE_API
|
||||
if [[ "$FILE_PATH" =~ \.(ts|vue)$ ]] || [[ "$FILE_PATH" =~ \.env ]]; then
|
||||
if grep -qE 'VITE_API_BASE_URL|VITE_BASE_API' "$FILE_PATH"; then
|
||||
LINE=$(grep -nE 'VITE_API_BASE_URL|VITE_BASE_API' "$FILE_PATH" | head -1)
|
||||
ISSUES="${ISSUES}[wrong-env-var] $FILE_PATH: wrong env var ($LINE) — must be VITE_APP_BASE_API. "
|
||||
fi
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# P07: POM 无 ${latest.version} 占位符
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ "$FILE_PATH" =~ pom\.xml$ ]]; then
|
||||
LATEST_HITS=$(grep -n '\${latest.version}' "$FILE_PATH" 2>/dev/null || true)
|
||||
if [[ -n "$LATEST_HITS" ]]; then
|
||||
FIRST_HIT=$(echo "$LATEST_HITS" | head -1)
|
||||
add_issue "[P07] $FILE_PATH contains \${latest.version} placeholder — use hard-coded version number. Line: $FIRST_HIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# P08: 业务表 SQL 有审计列(轻量 — 只检查当前文件)
|
||||
# 7 列: tenant_id, created_by, created_time, updated_by, updated_time, deleted, version
|
||||
# Quartz 表 (QRTZ_) 豁免
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ "$FILE_PATH" =~ \.sql$ && "$FILE_PATH" =~ migration/ ]]; then
|
||||
AUDIT_COLS=(tenant_id created_by created_time updated_by updated_time deleted version)
|
||||
# 找所有 CREATE TABLE 语句
|
||||
TABLES=$(grep -oPI 'CREATE\s+TABLE\s+\K\S+' "$FILE_PATH" 2>/dev/null || true)
|
||||
for TABLE in $TABLES; do
|
||||
TABLE_LOWER=$(echo "$TABLE" | tr '[:upper:]' '[:lower:]')
|
||||
# 跳过 Quartz 表
|
||||
if [[ "$TABLE_LOWER" == *"qrtz_"* ]]; then
|
||||
# Rule no-preauthorize: Controller @*Mapping must have @PreAuthorize
|
||||
if [[ "$FILE_PATH" =~ Controller\.java$ ]]; then
|
||||
MAPPING_LINES=$(grep -n '@\(Get\|Post\|Put\|Delete\|Patch\)Mapping' "$FILE_PATH" | cut -d: -f1 || true)
|
||||
for LINE_NUM in $MAPPING_LINES; do
|
||||
LINE_CONTENT=$(sed -n "${LINE_NUM}p" "$FILE_PATH")
|
||||
if echo "$LINE_CONTENT" | grep -qiE '(login|refresh)'; then
|
||||
continue
|
||||
fi
|
||||
TABLE_BLOCK=$(sed -n "/CREATE TABLE.*${TABLE}/I,/);/p" "$FILE_PATH" 2>/dev/null || true)
|
||||
MISSING_COLS=""
|
||||
for col in "${AUDIT_COLS[@]}"; do
|
||||
if ! echo "$TABLE_BLOCK" | grep -qi "$col"; then
|
||||
if [[ -n "$MISSING_COLS" ]]; then
|
||||
MISSING_COLS+=", "
|
||||
fi
|
||||
MISSING_COLS+="$col"
|
||||
fi
|
||||
done
|
||||
if [[ -n "$MISSING_COLS" ]]; then
|
||||
add_issue "[P08] $FILE_PATH table $TABLE missing audit columns: $MISSING_COLS"
|
||||
START=$((LINE_NUM > 5 ? LINE_NUM - 5 : 1))
|
||||
CONTEXT=$(sed -n "${START},${LINE_NUM}p" "$FILE_PATH")
|
||||
if echo "$CONTEXT" | grep -q '@PreAuthorize'; then
|
||||
: # has @PreAuthorize, ok
|
||||
else
|
||||
ISSUES="${ISSUES}[no-preauthorize] $FILE_PATH:$LINE_NUM: @*Mapping without @PreAuthorize. "
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# P09: Java import 不违反 DAG
|
||||
# core 不能 import security/data/web
|
||||
# security 不能 import web
|
||||
# data 不能 import security/web
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ "$FILE_PATH" =~ \.java$ ]]; then
|
||||
if [[ "$FILE_PATH" =~ ^ljwx-platform-core/ ]]; then
|
||||
BAD_IMPORT=$(grep -n 'import.*ljwx.*\(security\|data\|web\)\.' "$FILE_PATH" 2>/dev/null || true)
|
||||
if [[ -n "$BAD_IMPORT" ]]; then
|
||||
FIRST_BAD=$(echo "$BAD_IMPORT" | head -1)
|
||||
add_issue "[P09] $FILE_PATH DAG violation: core module imports from security/data/web. Line: $FIRST_BAD"
|
||||
fi
|
||||
fi
|
||||
if [[ "$FILE_PATH" =~ ^ljwx-platform-security/ ]]; then
|
||||
BAD_IMPORT=$(grep -n 'import.*ljwx.*web\.' "$FILE_PATH" 2>/dev/null || true)
|
||||
if [[ -n "$BAD_IMPORT" ]]; then
|
||||
FIRST_BAD=$(echo "$BAD_IMPORT" | head -1)
|
||||
add_issue "[P09] $FILE_PATH DAG violation: security module imports from web. Line: $FIRST_BAD"
|
||||
fi
|
||||
fi
|
||||
if [[ "$FILE_PATH" =~ ^ljwx-platform-data/ ]]; then
|
||||
BAD_IMPORT=$(grep -n 'import.*ljwx.*\(security\|web\)\.' "$FILE_PATH" 2>/dev/null || true)
|
||||
if [[ -n "$BAD_IMPORT" ]]; then
|
||||
FIRST_BAD=$(echo "$BAD_IMPORT" | head -1)
|
||||
add_issue "[P09] $FILE_PATH DAG violation: data module imports from security/web. Line: $FIRST_BAD"
|
||||
fi
|
||||
# Rule no-latest-version: POM must not use ${latest.version}
|
||||
if [[ "$FILE_PATH" =~ pom\.xml$ ]]; then
|
||||
if grep -q '\${latest\.version}' "$FILE_PATH"; then
|
||||
LINE=$(grep -n '\${latest\.version}' "$FILE_PATH" | head -1)
|
||||
ISSUES="${ISSUES}[no-latest-version] $FILE_PATH: \${latest.version} found ($LINE). "
|
||||
fi
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# P10: Vue Router v5 API(标记已废弃的 v4 用法)
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ "$FILE_PATH" =~ \.(ts|vue)$ ]]; then
|
||||
V4_DEPRECATED=(
|
||||
'onBeforeRouteLeave'
|
||||
'onBeforeRouteUpdate'
|
||||
'useLink'
|
||||
)
|
||||
for pattern in "${V4_DEPRECATED[@]}"; do
|
||||
V4_HIT=$(grep -n "$pattern" "$FILE_PATH" 2>/dev/null | grep -v '^\s*//' | head -1 || true)
|
||||
if [[ -n "$V4_HIT" ]]; then
|
||||
add_issue "[P10] $FILE_PATH uses deprecated Vue Router v4 API '$pattern' — use v5 equivalent. See https://router.vuejs.org/guide/migration/v4-to-v5. Line: $V4_HIT"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 输出结果
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ -n "$ISSUES" ]]; then
|
||||
# 转义 JSON 特殊字符
|
||||
ESCAPED_ISSUES=$(echo "$ISSUES" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/ /g')
|
||||
echo "{\"decision\":\"block\",\"reason\":\"$ESCAPED_ISSUES\"}"
|
||||
exit 0
|
||||
SAFE=$(printf '%s' "$ISSUES" | sed 's/"/\\"/g')
|
||||
printf '{"decision":"block","reason":"%s"}\n' "$SAFE"
|
||||
fi
|
||||
|
||||
# 无问题,静默通过
|
||||
exit 0
|
||||
|
||||
+30
-132
@@ -1,153 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# pre-edit-guard.sh — PreToolUse Hook for Edit|Write
|
||||
#
|
||||
# 在 Claude 写入/修改文件之前触发。
|
||||
# 只做路径级拦截,不解析 tool_input.content(避免依赖不稳定的 JSON 字段)。
|
||||
#
|
||||
# 拦截规则:
|
||||
# G01. Flyway 迁移文件必须在正确的目录
|
||||
# G02. DAG 守卫:模块目录级别的依赖方向
|
||||
# G03. 禁止直接修改 Quartz DDL 表(QRTZ_ 表由 Quartz 自管理)
|
||||
# G04. 禁止修改锁定文件(pnpm-lock.yaml 应由 pnpm install 生成)
|
||||
# G05. 禁止修改 .claude/ 配置(防止 Claude 修改自己的规则)
|
||||
# G06. 禁止在错误的 package 中创建前端文件
|
||||
#
|
||||
# 通信协议:
|
||||
# exit 0 = 允许写入
|
||||
# exit 2 = 阻止写入,stderr 内容反馈给 Claude
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# PreToolUse hook: path-level guard only.
|
||||
# Checks file_path from stdin JSON. No content parsing.
|
||||
# Exit 2 = block the tool call.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r ".tool_input.file_path // empty" 2>/dev/null || true)
|
||||
[[ -z "$FILE_PATH" ]] && exit 0
|
||||
|
||||
# 提取文件路径——这是 Edit/Write 工具唯一稳定的字段
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
||||
|
||||
# 如果拿不到路径(理论上不应发生),放行
|
||||
if [[ -z "$FILE_PATH" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 标准化路径:去掉开头的 ./
|
||||
FILE_PATH="${FILE_PATH#./}"
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# G01: Flyway 迁移文件目录守卫
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ "$FILE_PATH" =~ \.sql$ && "$FILE_PATH" =~ (V[0-9]|migration) ]]; then
|
||||
EXPECTED_DIR="ljwx-platform-app/src/main/resources/db/migration/"
|
||||
if [[ "$FILE_PATH" != ${EXPECTED_DIR}* ]]; then
|
||||
echo "BLOCKED [G01]: SQL migration file must be in $EXPECTED_DIR" >&2
|
||||
echo " Attempted path: $FILE_PATH" >&2
|
||||
echo " Move the file to the correct directory." >&2
|
||||
# Rule: Flyway migration must be in the correct directory
|
||||
if [[ "$FILE_PATH" =~ \.sql$ ]] && [[ "$FILE_PATH" =~ migration ]]; then
|
||||
if echo "$FILE_PATH" | grep -q "^ljwx-platform-app/src/main/resources/db/migration/"; then
|
||||
: # correct path
|
||||
else
|
||||
echo "BLOCKED: SQL migration must be in ljwx-platform-app/src/main/resources/db/migration/" >&2
|
||||
echo " Got: $FILE_PATH" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# G02: 模块 DAG 守卫
|
||||
# DAG: core → {security, data} → web → app
|
||||
#
|
||||
# 具体约束:
|
||||
# core 模块不能包含引用 security/data/web 的文件
|
||||
# security 模块不能包含引用 web 的文件
|
||||
# data 模块不能包含引用 security/web 的文件
|
||||
# (app 模块可以依赖所有模块)
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
# core 模块不能创建属于其他模块的文件
|
||||
if [[ "$FILE_PATH" =~ ^ljwx-platform-core/ ]]; then
|
||||
if [[ "$FILE_PATH" =~ /(security|web|controller|filter|interceptor)/ ]]; then
|
||||
echo "BLOCKED [G02]: core module must not contain security/web layer code." >&2
|
||||
echo " File: $FILE_PATH" >&2
|
||||
echo " DAG rule: core is the base module, it cannot reference security, data, or web." >&2
|
||||
echo " Move this file to the appropriate module (ljwx-platform-security or ljwx-platform-web)." >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# security 模块不能创建 web 层文件
|
||||
if [[ "$FILE_PATH" =~ ^ljwx-platform-security/ ]]; then
|
||||
if [[ "$FILE_PATH" =~ /(controller|web/controller|rest/)/ ]]; then
|
||||
echo "BLOCKED [G02]: security module must not contain controller/REST code." >&2
|
||||
echo " File: $FILE_PATH" >&2
|
||||
echo " DAG rule: security → web, not the reverse. Controllers belong in ljwx-platform-web." >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# data 模块不能创建 security 或 web 层文件
|
||||
# Rule: data module must not reference security
|
||||
if [[ "$FILE_PATH" =~ ^ljwx-platform-data/ ]]; then
|
||||
if [[ "$FILE_PATH" =~ /(security|auth|jwt|controller|web|rest/)/ ]]; then
|
||||
echo "BLOCKED [G02]: data module must not contain security or web layer code." >&2
|
||||
echo " File: $FILE_PATH" >&2
|
||||
echo " DAG rule: data module only depends on core. Security and web code belongs elsewhere." >&2
|
||||
if echo "$FILE_PATH" | grep -qi security; then
|
||||
echo "BLOCKED: data module must not reference security (DAG violation)" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# G03: 禁止手动创建/修改 Quartz DDL
|
||||
# Quartz 表由 Quartz 引擎根据配置自动初始化或由官方 DDL 导入
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ "$FILE_PATH" =~ \.sql$ ]]; then
|
||||
FILE_LOWER=$(echo "$FILE_PATH" | tr '[:upper:]' '[:lower:]')
|
||||
if [[ "$FILE_LOWER" =~ qrtz_ && ! "$FILE_LOWER" =~ migration/ ]]; then
|
||||
echo "BLOCKED [G03]: Do not manually create Quartz DDL files outside of Flyway migrations." >&2
|
||||
echo " File: $FILE_PATH" >&2
|
||||
echo " Quartz table DDL should be in a Flyway migration file (e.g., V003__init_quartz.sql)." >&2
|
||||
# Rule: security module must not contain web-layer code
|
||||
if [[ "$FILE_PATH" =~ ^ljwx-platform-security/ ]]; then
|
||||
if [[ "$FILE_PATH" =~ /web/ ]]; then
|
||||
echo "BLOCKED: security module must not contain web-layer code (DAG violation)" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# G04: 禁止直接修改生成的锁文件
|
||||
# ══════════════════════════════════════════════════════════
|
||||
LOCKED_FILES=(
|
||||
"pnpm-lock.yaml"
|
||||
"package-lock.json"
|
||||
"yarn.lock"
|
||||
)
|
||||
for locked in "${LOCKED_FILES[@]}"; do
|
||||
if [[ "$FILE_PATH" == *"$locked" ]]; then
|
||||
echo "BLOCKED [G04]: Do not directly edit $locked." >&2
|
||||
echo " This file is auto-generated by the package manager." >&2
|
||||
echo " Modify package.json instead, then run 'pnpm install'." >&2
|
||||
# Rule: deny direct edits to .env secret files (.env.example is a template and is allowed)
|
||||
BASENAME=$(basename "$FILE_PATH")
|
||||
case "$BASENAME" in
|
||||
.env.example)
|
||||
: # template file — allowed
|
||||
;;
|
||||
.env|.env.*)
|
||||
echo "BLOCKED: Direct edit of .env secret files not allowed." >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# G05: 禁止修改 .claude/ 配置
|
||||
# 防止 Claude 在执行过程中修改自己的权限或 hook 配置
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ "$FILE_PATH" =~ ^\.claude/ ]]; then
|
||||
echo "BLOCKED [G05]: Do not modify .claude/ configuration files during code generation." >&2
|
||||
echo " File: $FILE_PATH" >&2
|
||||
echo " These files control permissions and hooks. They must only be changed by humans." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# G06: 前端文件必须在正确的 package 中
|
||||
# admin → packages/admin/
|
||||
# mobile → packages/mobile/
|
||||
# screen → packages/screen/
|
||||
# shared → packages/shared/
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ "$FILE_PATH" =~ ^ljwx-platform-admin/ ]]; then
|
||||
if [[ "$FILE_PATH" =~ \.(vue|ts|tsx)$ ]]; then
|
||||
if [[ ! "$FILE_PATH" =~ ^ljwx-platform-admin/packages/ ]]; then
|
||||
echo "BLOCKED [G06]: Frontend source files must be inside ljwx-platform-admin/packages/." >&2
|
||||
echo " File: $FILE_PATH" >&2
|
||||
echo " Place it under packages/admin/, packages/mobile/, packages/screen/, or packages/shared/." >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 全部通过,允许写入
|
||||
# ══════════════════════════════════════════════════════════
|
||||
exit 0
|
||||
|
||||
+58
-134
@@ -1,170 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# stop-gate.sh — Stop Hook
|
||||
#
|
||||
# 每当 Claude 想停止响应时触发。
|
||||
# 检查当前 Phase 的最低完成标准,未满足则 block 让 Claude 继续。
|
||||
#
|
||||
# 设计原则(来自审查修订):
|
||||
# - 所有检查必须在 100ms 内完成
|
||||
# - 不跑编译、不跑 pnpm、不跑任何耗时操作
|
||||
# - 只做文件存在性检查 + 轻量 grep
|
||||
# - 编译/类型检查交给外层 gate 脚本
|
||||
# - 必须处理 stop_hook_active 防止无限循环
|
||||
#
|
||||
# 通信协议:
|
||||
# exit 0 + 无输出 = 允许停止
|
||||
# exit 0 + {"decision":"block","reason":""} = 阻止停止,reason 反馈给 Claude
|
||||
#
|
||||
# 关键安全阀:
|
||||
# stop_hook_active=true 时必须放行,否则无限循环
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# Stop hook — lightweight gate, all checks finish in <100ms.
|
||||
# NO compilation, NO pnpm, NO network. Only: file-exists, grep, git-diff.
|
||||
# Compilation is handled by the gate scripts inside phase-execute.sh.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 安全阀:防止无限循环
|
||||
# 当 Stop Hook 已经 block 过一次后,Claude 再次尝试停止时
|
||||
# stop_hook_active 会被设为 true。此时必须放行。
|
||||
# ══════════════════════════════════════════════════════════
|
||||
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null)
|
||||
# Prevent infinite loop: Claude Code sets stop_hook_active=true on retry
|
||||
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
|
||||
if [[ "$STOP_ACTIVE" == "true" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 确定当前 Phase
|
||||
# ══════════════════════════════════════════════════════════
|
||||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
||||
# Resolve project root
|
||||
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
CURRENT_PHASE=$(grep -oP 'Current Phase:\s*\K\d+' CLAUDE.md 2>/dev/null || echo "")
|
||||
|
||||
# 无法确定 Phase → 放行(可能在非 Phase 执行模式下)
|
||||
# Determine current phase number from CLAUDE.md
|
||||
CURRENT_PHASE=$(sed -n 's/^Phase:[[:space:]]*\([0-9][0-9]*\).*/\1/p' CLAUDE.md 2>/dev/null | head -1 || echo "")
|
||||
if [[ -z "$CURRENT_PHASE" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PHASE_PADDED=$(printf '%02d' "$CURRENT_PHASE")
|
||||
PHASE_BRIEF="spec/phase/phase-${PHASE_PADDED}.md"
|
||||
PHASE_BRIEF="spec/phase/phase-$(printf '%02d' "$CURRENT_PHASE").md"
|
||||
[[ -f "$PHASE_BRIEF" ]] || exit 0
|
||||
|
||||
# Phase Brief 不存在 → 放行
|
||||
if [[ ! -f "$PHASE_BRIEF" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
BLOCK_REASON=""
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 收集 block 原因
|
||||
# ══════════════════════════════════════════════════════════
|
||||
BLOCK_REASONS=""
|
||||
|
||||
add_block() {
|
||||
if [[ -n "$BLOCK_REASONS" ]]; then
|
||||
BLOCK_REASONS+=" | "
|
||||
fi
|
||||
BLOCK_REASONS+="$1"
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# Check 1: PHASE_MANIFEST.txt 有当前 Phase 的段落
|
||||
# 耗时:< 1ms
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ -f "PHASE_MANIFEST.txt" ]]; then
|
||||
# 支持多种 marker 格式
|
||||
if ! grep -qP "(## PHASE $CURRENT_PHASE\b|## Phase $CURRENT_PHASE\b)" PHASE_MANIFEST.txt 2>/dev/null; then
|
||||
add_block "PHASE_MANIFEST.txt missing section for Phase $CURRENT_PHASE. Write it before stopping."
|
||||
fi
|
||||
else
|
||||
add_block "PHASE_MANIFEST.txt not found. Create it and write the Phase $CURRENT_PHASE section."
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# Check 2: Phase Brief scope 中的具体文件是否都已创建
|
||||
# 耗时:< 10ms(通常 scope 不超过 20 个条目)
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# Check 1: Concrete scope files from Phase Brief YAML front-matter (<10ms)
|
||||
IN_SCOPE=false
|
||||
MISSING_FILES=()
|
||||
SCOPE_BLOCK=$(sed -n '/^scope:/,/^[a-z]/p' "$PHASE_BRIEF" 2>/dev/null | grep '^\s*-' || true)
|
||||
|
||||
if [[ -n "$SCOPE_BLOCK" ]]; then
|
||||
while IFS= read -r line; do
|
||||
# 提取路径
|
||||
ENTRY=$(echo "$line" | sed 's/^\s*-\s*//; s/^"//; s/"$//' | xargs)
|
||||
[[ -z "$ENTRY" ]] && continue
|
||||
# 跳过 glob 模式
|
||||
if [[ "$ENTRY" == *"*"* || "$ENTRY" == *"**"* ]]; then
|
||||
continue
|
||||
while IFS= read -r line; do
|
||||
if echo "$line" | grep -q '^scope:'; then
|
||||
IN_SCOPE=true
|
||||
continue
|
||||
fi
|
||||
if $IN_SCOPE; then
|
||||
if echo "$line" | grep -qE '^[[:space:]]*-[[:space:]]+'; then
|
||||
entry=$(echo "$line" | sed 's/^[[:space:]]*-[[:space:]]*//' | sed 's/^"//' | sed 's/"$//')
|
||||
# Only check concrete paths (no glob wildcards)
|
||||
if echo "$entry" | grep -qF '*'; then
|
||||
: # has wildcard, skip
|
||||
elif [[ "$(basename "$entry")" == .env* ]]; then
|
||||
: # .env* files cannot be written by Claude Code (system sandbox); user creates manually
|
||||
else
|
||||
[[ -f "$entry" ]] || MISSING_FILES+=("$entry")
|
||||
fi
|
||||
else
|
||||
IN_SCOPE=false
|
||||
fi
|
||||
# 检查文件是否存在
|
||||
if [[ ! -f "$ENTRY" && ! -d "$ENTRY" ]]; then
|
||||
MISSING_FILES+=("$ENTRY")
|
||||
fi
|
||||
done <<< "$SCOPE_BLOCK"
|
||||
fi
|
||||
fi
|
||||
done < <(awk '/^---$/{n++;if(n==1){next};if(n==2){exit}} n==1{print}' "$PHASE_BRIEF")
|
||||
|
||||
if [[ ${#MISSING_FILES[@]} -gt 0 ]]; then
|
||||
# 最多报告 5 个缺失文件,避免 reason 过长
|
||||
SHOW_COUNT=5
|
||||
TOTAL=${#MISSING_FILES[@]}
|
||||
DISPLAY=("${MISSING_FILES[@]:0:$SHOW_COUNT}")
|
||||
MISSING_LIST=$(printf '%s, ' "${DISPLAY[@]}")
|
||||
MISSING_LIST="${MISSING_LIST%, }" # 去尾逗号
|
||||
if [[ $TOTAL -gt $SHOW_COUNT ]]; then
|
||||
MISSING_LIST+=" ... and $((TOTAL - SHOW_COUNT)) more"
|
||||
fi
|
||||
add_block "Missing $TOTAL expected files: $MISSING_LIST. Create them before stopping."
|
||||
BLOCK_REASON="Missing expected scope files: $(printf '%s ' "${MISSING_FILES[@]}")"
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# Check 3: 变更文件中的明显违规(秒级 grep)
|
||||
# 只检查最容易犯的 3 个错误,完整检查交给 gate-rules.sh
|
||||
# 耗时:< 50ms
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# Check 2: Quick grep on git-changed files (<50ms)
|
||||
CHANGED=$(git diff --name-only HEAD 2>/dev/null || true)
|
||||
if [[ -n "$CHANGED" ]]; then
|
||||
|
||||
# 3a: package.json 中的 caret
|
||||
CARET_FILES=$(echo "$CHANGED" | grep 'package\.json$' || true)
|
||||
for f in $CARET_FILES; do
|
||||
[[ -f "$f" ]] || continue
|
||||
if grep -q '"\^' "$f" 2>/dev/null; then
|
||||
add_block "$f contains caret (^) versions. Replace all ^ with ~ before stopping."
|
||||
break # 报告一次即可
|
||||
# Caret in package.json
|
||||
while IFS= read -r f; do
|
||||
[[ -z "$f" ]] && continue
|
||||
if [[ -f "$f" ]] && grep -q '"^' "$f" 2>/dev/null; then
|
||||
BLOCK_REASON="${BLOCK_REASON}Caret (^) version found in $f. "
|
||||
fi
|
||||
done
|
||||
done < <(echo "$CHANGED" | grep 'package\.json' || true)
|
||||
|
||||
# 3b: SQL 中的 IF NOT EXISTS
|
||||
SQL_FILES=$(echo "$CHANGED" | grep '\.sql$' || true)
|
||||
for f in $SQL_FILES; do
|
||||
[[ -f "$f" ]] || continue
|
||||
if grep -qi 'IF NOT EXISTS' "$f" 2>/dev/null; then
|
||||
add_block "$f contains IF NOT EXISTS. Remove it — Flyway handles migration state."
|
||||
break
|
||||
# IF NOT EXISTS in SQL migration files
|
||||
while IFS= read -r f; do
|
||||
[[ -z "$f" ]] && continue
|
||||
if [[ -f "$f" ]] && grep -qi 'IF NOT EXISTS' "$f" 2>/dev/null; then
|
||||
BLOCK_REASON="${BLOCK_REASON}IF NOT EXISTS in $f (Flyway rule). "
|
||||
fi
|
||||
done
|
||||
done < <(echo "$CHANGED" | grep '\.sql$' || true)
|
||||
|
||||
# 3c: DTO 中的 tenantId
|
||||
DTO_FILES=$(echo "$CHANGED" | grep -P '(DTO|Dto|Request|Response)\.java$' || true)
|
||||
for f in $DTO_FILES; do
|
||||
[[ -f "$f" ]] || continue
|
||||
if grep -q 'tenantId\|tenant_id' "$f" 2>/dev/null | grep -v 'import ' | grep -v '^\s*//' | head -1 > /dev/null 2>&1; then
|
||||
if grep 'tenantId\|tenant_id' "$f" 2>/dev/null | grep -v 'import ' | grep -v '^\s*//' | head -1 > /dev/null 2>&1; then
|
||||
add_block "$f is a DTO that exposes tenant_id. Remove the tenantId field."
|
||||
break
|
||||
fi
|
||||
# Wrong env var in TS/Vue files
|
||||
while IFS= read -r f; do
|
||||
[[ -z "$f" ]] && continue
|
||||
if [[ -f "$f" ]] && grep -qE 'VITE_API_BASE_URL|VITE_BASE_API' "$f" 2>/dev/null; then
|
||||
BLOCK_REASON="${BLOCK_REASON}Wrong env var in $f (must be VITE_APP_BASE_API). "
|
||||
fi
|
||||
done
|
||||
done < <(echo "$CHANGED" | grep -E '\.(ts|vue)$' || true)
|
||||
fi
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 决定是否阻止停止
|
||||
# ══════════════════════════════════════════════════════════
|
||||
if [[ -n "$BLOCK_REASONS" ]]; then
|
||||
# 转义 JSON
|
||||
ESCAPED=$(echo "$BLOCK_REASONS" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/ /g')
|
||||
echo "{\"decision\":\"block\",\"reason\":\"Cannot stop yet: $ESCAPED\"}"
|
||||
if [[ -n "$BLOCK_REASON" ]]; then
|
||||
SAFE=$(printf '%s' "$BLOCK_REASON" | sed 's/"/\\"/g')
|
||||
printf '{"decision":"block","reason":"Stop gate: %s"}\n' "$SAFE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 全部通过,允许停止
|
||||
exit 0
|
||||
|
||||
+172
-57
@@ -1,24 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# phase-execute.sh — 单 Phase 执行器(含重试、manifest 写入、追溯日志)
|
||||
# 用法: bash scripts/phase-execute.sh <phase-number> [--skip-preflight]
|
||||
#
|
||||
# 流程: preflight → 解析 Phase Brief → Claude 生成 → gate-all → 重试(×3)
|
||||
# → diff-review → code-reviewer → 写 PHASE_MANIFEST.txt → git commit
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
set -euo pipefail
|
||||
|
||||
PHASE_NUM="${1:?Usage: phase-execute.sh <phase-number> [--skip-preflight]}"
|
||||
SKIP_PREFLIGHT="${2:-}"
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
export CLAUDE_PROJECT_DIR="$PROJECT_ROOT"
|
||||
|
||||
PHASE_PADDED=$(printf '%02d' "$PHASE_NUM")
|
||||
PHASE_BRIEF="spec/phase/phase-${PHASE_PADDED}.md"
|
||||
LOG_DIR="logs/phase-${PHASE_PADDED}"
|
||||
MAX_RETRIES=3
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
PHASE_BRIEF="spec/phase/phase-$(printf '%02d' "$PHASE_NUM").md"
|
||||
if [[ ! -f "$PHASE_BRIEF" ]]; then
|
||||
echo "ERROR: $PHASE_BRIEF not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Parse Phase Brief ─────────────────────────────────────
|
||||
# ── Parse Phase Brief ──────────────────────────────────────
|
||||
source scripts/lib/parse-phase-brief.sh "$PHASE_BRIEF"
|
||||
echo "════════════════════════════════════════"
|
||||
echo "Phase $PHASE_NUM: $PHASE_TITLE"
|
||||
echo " Backend: $TARGET_BACKEND | Frontend: $TARGET_FRONTEND"
|
||||
echo " Scope: ${PHASE_SCOPE[*]}"
|
||||
echo " Scope files: ${#PHASE_SCOPE[@]}"
|
||||
echo "════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
# ── Update CLAUDE.md current phase marker ─────────────────
|
||||
# Preserves the title portion: "Phase: 0 (Skeleton) — ..." → "Phase: N ..."
|
||||
sed -i.bak "s/^Phase: [0-9][0-9]*/Phase: $PHASE_NUM/" CLAUDE.md && rm -f CLAUDE.md.bak
|
||||
|
||||
# ── Step 0: Preflight ─────────────────────────────────────
|
||||
if [[ "$SKIP_PREFLIGHT" != "--skip-preflight" ]]; then
|
||||
echo "═══ STEP 0: Preflight Check ═══"
|
||||
@@ -26,83 +46,178 @@ if [[ "$SKIP_PREFLIGHT" != "--skip-preflight" ]]; then
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── Step 1a: Backend generation ───────────────────────────
|
||||
if [[ "$TARGET_BACKEND" == "true" ]]; then
|
||||
echo "═══ STEP 1a: Backend Builder — Phase $PHASE_NUM ═══"
|
||||
claude -p "You are backend-builder. Execute Phase $PHASE_NUM.
|
||||
Read CLAUDE.md, spec/01-constraints.md, and $PHASE_BRIEF.
|
||||
Generate all backend files listed in the Phase Brief scope.
|
||||
Follow spec/08-output-rules.md for output format.
|
||||
After generating, run: bash scripts/gates/gate-compile.sh" \
|
||||
--agent backend-builder \
|
||||
--output-format json > "/tmp/ljwx-phase-${PHASE_NUM}-backend.json"
|
||||
echo "Backend generation complete."
|
||||
echo ""
|
||||
fi
|
||||
# ── Build generation prompt ───────────────────────────────
|
||||
BUILD_PROMPT="You are executing Phase $PHASE_NUM of the LJWX Platform.
|
||||
|
||||
# ── Step 1b: Frontend generation ──────────────────────────
|
||||
if [[ "$TARGET_FRONTEND" == "true" ]]; then
|
||||
echo "═══ STEP 1b: Frontend Builder — Phase $PHASE_NUM ═══"
|
||||
claude -p "You are frontend-builder. Execute Phase $PHASE_NUM.
|
||||
Read CLAUDE.md, spec/06-frontend-config.md, and $PHASE_BRIEF.
|
||||
Generate all frontend files listed in the Phase Brief scope.
|
||||
Follow spec/08-output-rules.md for output format.
|
||||
After generating, run: pnpm install && pnpm run type-check" \
|
||||
--agent frontend-builder \
|
||||
--output-format json > "/tmp/ljwx-phase-${PHASE_NUM}-frontend.json"
|
||||
echo "Frontend generation complete."
|
||||
echo ""
|
||||
fi
|
||||
Phase title: $PHASE_TITLE
|
||||
Backend: $TARGET_BACKEND | Frontend: $TARGET_FRONTEND
|
||||
|
||||
# ── Step 2: Gate check ────────────────────────────────────
|
||||
echo "═══ STEP 2: Gate Check ═══"
|
||||
bash scripts/gates/gate-all.sh
|
||||
GATE_EXIT=$?
|
||||
if [[ "$GATE_EXIT" -ne 0 ]]; then
|
||||
echo "GATE FAILED — aborting."
|
||||
## Your task
|
||||
1. Read CLAUDE.md for all hard rules
|
||||
2. Read spec/phase/${PHASE_BRIEF} for scope, deliverables, and acceptance criteria
|
||||
3. Read only the spec files referenced in the Phase Brief reading list
|
||||
4. Generate EVERY file listed in the scope section — complete content, no placeholders
|
||||
5. After generating backend files: run mvn clean compile -f pom.xml and fix all errors
|
||||
6. After generating frontend files: run pnpm run type-check and fix all type errors
|
||||
7. Write a PHASE_MANIFEST.txt entry in this exact format:
|
||||
|
||||
## PHASE $PHASE_NUM
|
||||
Title: $PHASE_TITLE
|
||||
Completed: \$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
Files:
|
||||
$(for f in "${PHASE_SCOPE[@]}"; do echo " - $f"; done)
|
||||
Status: PENDING
|
||||
|
||||
8. Then run: bash scripts/gates/gate-all.sh $PHASE_NUM
|
||||
9. If any gate FAILS, fix every FAIL and re-run gates until all PASS
|
||||
10. Once gates PASS, update the Status in PHASE_MANIFEST.txt to: PASSED
|
||||
11. Do NOT stop until all gates pass and PHASE_MANIFEST.txt is updated
|
||||
|
||||
Follow spec/08-output-rules.md for output format strictly."
|
||||
|
||||
# ── Step 1-2: Generation with retry ───────────────────────
|
||||
RETRY=0
|
||||
GATE_PASSED=false
|
||||
|
||||
while [[ $RETRY -lt $MAX_RETRIES ]]; do
|
||||
ATTEMPT=$((RETRY + 1))
|
||||
echo "═══ STEP 1: Generation — Attempt $ATTEMPT / $MAX_RETRIES ═══"
|
||||
|
||||
ATTEMPT_LOG="$LOG_DIR/attempt-${ATTEMPT}.log"
|
||||
|
||||
if [[ $RETRY -eq 0 ]]; then
|
||||
PROMPT="$BUILD_PROMPT"
|
||||
else
|
||||
# Collect gate failures for retry prompt
|
||||
GATE_OUTPUT=$(bash scripts/gates/gate-all.sh "$PHASE_NUM" 2>&1 || true)
|
||||
PROMPT="Phase $PHASE_NUM gate check FAILED on attempt $RETRY. Fix ALL issues below:
|
||||
|
||||
$GATE_OUTPUT
|
||||
|
||||
After fixing:
|
||||
1. Re-run: bash scripts/gates/gate-all.sh $PHASE_NUM
|
||||
2. Ensure ALL gates PASS
|
||||
3. Update PHASE_MANIFEST.txt Status to PASSED
|
||||
4. Do not stop until gates pass."
|
||||
fi
|
||||
|
||||
# Run claude in headless mode
|
||||
# --agent flag is used to activate the agent definition from .claude/agents/
|
||||
if [[ "$TARGET_BACKEND" == "true" && "$TARGET_FRONTEND" != "true" ]]; then
|
||||
claude -p "$PROMPT" \
|
||||
--agent backend-builder \
|
||||
--max-turns 80 \
|
||||
--output-format text \
|
||||
2>&1 | tee "$ATTEMPT_LOG"
|
||||
elif [[ "$TARGET_FRONTEND" == "true" && "$TARGET_BACKEND" != "true" ]]; then
|
||||
claude -p "$PROMPT" \
|
||||
--agent frontend-builder \
|
||||
--max-turns 60 \
|
||||
--output-format text \
|
||||
2>&1 | tee "$ATTEMPT_LOG"
|
||||
else
|
||||
# Both or neither: use no specific agent (full Claude)
|
||||
claude -p "$PROMPT" \
|
||||
--max-turns 80 \
|
||||
--output-format text \
|
||||
2>&1 | tee "$ATTEMPT_LOG"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "═══ STEP 2: Gate Check — Attempt $ATTEMPT ═══"
|
||||
if bash scripts/gates/gate-all.sh "$PHASE_NUM" 2>&1 | tee "$LOG_DIR/gate-attempt-${ATTEMPT}.log"; then
|
||||
GATE_PASSED=true
|
||||
echo "Gates PASSED on attempt $ATTEMPT."
|
||||
break
|
||||
else
|
||||
echo "Gates FAILED on attempt $ATTEMPT."
|
||||
RETRY=$((RETRY + 1))
|
||||
if [[ $RETRY -ge $MAX_RETRIES ]]; then
|
||||
echo "All $MAX_RETRIES attempts exhausted. Phase $PHASE_NUM FAILED."
|
||||
echo "Phase $PHASE_NUM FAILED — $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> logs/failed.log
|
||||
exit 1
|
||||
fi
|
||||
echo "Retrying with gate error feedback..."
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$GATE_PASSED" != "true" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ── Step 3: Diff-driven hard rule scan ────────────────────
|
||||
echo "═══ STEP 3: Diff Review (zero-token) ═══"
|
||||
bash scripts/review/diff-review.sh "$PHASE_NUM"
|
||||
REVIEW_EXIT=$?
|
||||
# ── Step 3: Zero-token diff review ────────────────────────
|
||||
echo "═══ STEP 3: Diff Review (zero-token rules scan) ═══"
|
||||
bash scripts/review/diff-review.sh "$PHASE_NUM" 2>&1 | tee "$LOG_DIR/diff-review.log"
|
||||
REVIEW_EXIT=${PIPESTATUS[0]}
|
||||
if [[ "$REVIEW_EXIT" -ne 0 ]]; then
|
||||
echo "DIFF REVIEW FAILED — aborting."
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ── Step 4: Semantic review via Agent ─────────────────────
|
||||
# ── Step 4: Semantic review via code-reviewer agent ───────
|
||||
echo "═══ STEP 4: Semantic Review ═══"
|
||||
CHANGED_FILES=$(git diff --name-only HEAD)
|
||||
REVIEW_RESULT=$(claude -p "You are code-reviewer. Review ONLY these changed files:
|
||||
CHANGED_FILES=$(git diff --name-only HEAD 2>/dev/null || true)
|
||||
if [[ -n "$CHANGED_FILES" ]]; then
|
||||
REVIEW_RESULT=$(claude -p "You are the code-reviewer. Review ONLY these changed files:
|
||||
$CHANGED_FILES
|
||||
|
||||
Phase $PHASE_NUM scope allows: ${PHASE_SCOPE[*]}
|
||||
Phase $PHASE_NUM allowed scope:
|
||||
$(for f in "${PHASE_SCOPE[@]}"; do echo " $f"; done)
|
||||
|
||||
Read CLAUDE.md and spec/01-constraints.md.
|
||||
For each file, check the review checklist.
|
||||
Flag any file NOT in the allowed scope as CRITICAL.
|
||||
Output each finding as:
|
||||
[CRITICAL|WARNING|INFO] <file>:<line> — <rule-id> — <description> — <evidence>
|
||||
Check all hard rules. Flag files outside scope as CRITICAL (scope-violation).
|
||||
Output: [CRITICAL|WARNING|INFO] <file>:<line> — <rule-id> — <description>
|
||||
End with: REVIEW PASSED or REVIEW FAILED: N critical, M warnings." \
|
||||
--agent code-reviewer \
|
||||
--output-format text)
|
||||
--agent code-reviewer \
|
||||
--max-turns 20 \
|
||||
--output-format text \
|
||||
2>&1 | tee "$LOG_DIR/semantic-review.log")
|
||||
|
||||
echo "$REVIEW_RESULT"
|
||||
echo ""
|
||||
echo "$REVIEW_RESULT"
|
||||
echo ""
|
||||
|
||||
if echo "$REVIEW_RESULT" | grep -q "REVIEW FAILED"; then
|
||||
exit 1
|
||||
if echo "$REVIEW_RESULT" | grep -q "REVIEW FAILED"; then
|
||||
echo "Semantic review found CRITICAL issues — aborting."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No changed files to review."
|
||||
fi
|
||||
|
||||
# ── Step 5: Commit ────────────────────────────────────────
|
||||
echo "═══ STEP 5: Git Commit ═══"
|
||||
# ── Step 5: Ensure PHASE_MANIFEST.txt is written ──────────
|
||||
echo "═══ STEP 5: PHASE_MANIFEST.txt ═══"
|
||||
MANIFEST="PHASE_MANIFEST.txt"
|
||||
MARKER="## PHASE $PHASE_NUM"
|
||||
|
||||
if grep -q "$MARKER" "$MANIFEST" 2>/dev/null; then
|
||||
# Update status to PASSED if not already
|
||||
sed -i.bak "/$MARKER/,/^## PHASE/{s/Status: PENDING/Status: PASSED/}" "$MANIFEST" && rm -f "${MANIFEST}.bak"
|
||||
echo " Manifest entry updated: $MARKER → PASSED"
|
||||
else
|
||||
# Write new entry
|
||||
{
|
||||
echo ""
|
||||
echo "$MARKER"
|
||||
echo "Title: $PHASE_TITLE"
|
||||
echo "Completed: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo "Files:"
|
||||
for f in "${PHASE_SCOPE[@]}"; do
|
||||
echo " - $f"
|
||||
done
|
||||
echo "Status: PASSED"
|
||||
} >> "$MANIFEST"
|
||||
echo " Manifest entry written: $MARKER"
|
||||
fi
|
||||
|
||||
# ── Step 6: Git commit ────────────────────────────────────
|
||||
echo "═══ STEP 6: Git Commit ═══"
|
||||
git add -A
|
||||
git commit -m "feat(phase-${PHASE_NUM}): ${PHASE_TITLE} — auto-verified"
|
||||
git commit -m "feat(phase-${PHASE_NUM}): ${PHASE_TITLE} — auto-verified" || true
|
||||
echo ""
|
||||
|
||||
echo "════════════════════════════════════════"
|
||||
echo "Phase $PHASE_NUM COMPLETE: $PHASE_TITLE"
|
||||
echo "Logs: $LOG_DIR/"
|
||||
echo "════════════════════════════════════════"
|
||||
|
||||
+22
-14
@@ -16,17 +16,22 @@ if [[ "$TARGET_BACKEND" != "true" || "$TARGET_FRONTEND" != "true" ]]; then
|
||||
fi
|
||||
|
||||
MAIN_BRANCH=$(git branch --show-current)
|
||||
BE_BRANCH="phase-${PHASE_NUM}-backend"
|
||||
FE_BRANCH="phase-${PHASE_NUM}-frontend"
|
||||
BE_WORKTREE="/tmp/ljwx-worktree-be-${PHASE_NUM}"
|
||||
FE_WORKTREE="/tmp/ljwx-worktree-fe-${PHASE_NUM}"
|
||||
# Record current HEAD before any merges — used for precise squash
|
||||
PRE_MERGE_HEAD=$(git rev-parse HEAD)
|
||||
|
||||
BE_WT="phase${PHASE_NUM}-be"
|
||||
FE_WT="phase${PHASE_NUM}-fe"
|
||||
BE_BRANCH="worktree-${BE_WT}"
|
||||
FE_BRANCH="worktree-${FE_WT}"
|
||||
BE_DIR=".claude/worktrees/${BE_WT}"
|
||||
FE_DIR=".claude/worktrees/${FE_WT}"
|
||||
|
||||
# ── Cleanup on exit ───────────────────────────────────────
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "Cleaning up worktrees..."
|
||||
git worktree remove "$BE_WORKTREE" --force 2>/dev/null || true
|
||||
git worktree remove "$FE_WORKTREE" --force 2>/dev/null || true
|
||||
git worktree remove "$BE_DIR" --force 2>/dev/null || true
|
||||
git worktree remove "$FE_DIR" --force 2>/dev/null || true
|
||||
git branch -D "$BE_BRANCH" 2>/dev/null || true
|
||||
git branch -D "$FE_BRANCH" 2>/dev/null || true
|
||||
}
|
||||
@@ -39,19 +44,20 @@ echo ""
|
||||
|
||||
# ── Step 1: Create isolated worktrees ─────────────────────
|
||||
echo "═══ Creating worktrees ═══"
|
||||
mkdir -p .claude/worktrees
|
||||
git branch "$BE_BRANCH" HEAD
|
||||
git branch "$FE_BRANCH" HEAD
|
||||
git worktree add "$BE_WORKTREE" "$BE_BRANCH"
|
||||
git worktree add "$FE_WORKTREE" "$FE_BRANCH"
|
||||
echo " Backend worktree: $BE_WORKTREE ($BE_BRANCH)"
|
||||
echo " Frontend worktree: $FE_WORKTREE ($FE_BRANCH)"
|
||||
git worktree add "$BE_DIR" "$BE_BRANCH"
|
||||
git worktree add "$FE_DIR" "$FE_BRANCH"
|
||||
echo " Backend worktree: $BE_DIR ($BE_BRANCH)"
|
||||
echo " Frontend worktree: $FE_DIR ($FE_BRANCH)"
|
||||
echo ""
|
||||
|
||||
# ── Step 2: Parallel generation ───────────────────────────
|
||||
echo "═══ Parallel Generation — Phase $PHASE_NUM ═══"
|
||||
|
||||
(
|
||||
cd "$BE_WORKTREE"
|
||||
cd "$BE_DIR"
|
||||
echo "[BE] Starting backend generation..."
|
||||
claude -p "You are backend-builder. Execute Phase $PHASE_NUM backend tasks.
|
||||
Read CLAUDE.md, spec/01-constraints.md, spec/08-output-rules.md, and $PHASE_BRIEF.
|
||||
@@ -67,7 +73,7 @@ Follow spec/08-output-rules.md for output format." \
|
||||
PID_BE=$!
|
||||
|
||||
(
|
||||
cd "$FE_WORKTREE"
|
||||
cd "$FE_DIR"
|
||||
echo "[FE] Starting frontend generation..."
|
||||
claude -p "You are frontend-builder. Execute Phase $PHASE_NUM frontend tasks.
|
||||
Read CLAUDE.md, spec/06-frontend-config.md, spec/08-output-rules.md, and $PHASE_BRIEF.
|
||||
@@ -150,9 +156,11 @@ if echo "$REVIEW_RESULT" | grep -q "REVIEW FAILED"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Step 7: Squash merge commits into one ─────────────────
|
||||
# ── Step 7: Squash all merge commits into one ─────────────
|
||||
# Use PRE_MERGE_HEAD (recorded before merges) for precise squash
|
||||
# This avoids the fragile HEAD~N counting
|
||||
echo "═══ Final Commit ═══"
|
||||
git reset --soft HEAD~2
|
||||
git reset --soft "$PRE_MERGE_HEAD"
|
||||
git commit -m "feat(phase-${PHASE_NUM}): ${PHASE_TITLE} — parallel verified"
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -255,16 +255,16 @@ if [[ -f "$CONSTRAINTS" ]]; then
|
||||
"deleted"
|
||||
"version"
|
||||
"HS256"
|
||||
"30.*min\|1800"
|
||||
"7.*day\|604800"
|
||||
"30.*min|1800"
|
||||
"7.*day|604800"
|
||||
"QRTZ_"
|
||||
"4096"
|
||||
"50.*MB\|52428800"
|
||||
"Caffeine\|caffeine"
|
||||
"strict.*true\|strict: true"
|
||||
"50.*MB|52428800"
|
||||
"caffeine"
|
||||
"strict: true"
|
||||
)
|
||||
for kw in "${CONSTRAINT_KEYS[@]}"; do
|
||||
if echo "$CONSTRAINT_CONTENT" | grep -qiP "$kw"; then
|
||||
if echo "$CONSTRAINT_CONTENT" | grep -qiE "$kw"; then
|
||||
pass "D constraints contains: $kw"
|
||||
else
|
||||
fail "D constraints MISSING: $kw"
|
||||
@@ -285,7 +285,7 @@ if [[ -f "$CONSTRAINTS" ]]; then
|
||||
"Element Plus.*2\.13"
|
||||
)
|
||||
for vk in "${VERSION_KEYS[@]}"; do
|
||||
if echo "$CONSTRAINT_CONTENT" | grep -qP "$vk"; then
|
||||
if echo "$CONSTRAINT_CONTENT" | grep -qE "$vk"; then
|
||||
pass "D version defined: $vk"
|
||||
else
|
||||
warn "D version not found in constraints: $vk"
|
||||
@@ -331,12 +331,12 @@ for i in $(seq 0 19); do
|
||||
done
|
||||
|
||||
# E3: targets 有 backend 和 frontend 布尔值
|
||||
if echo "$FRONT_MATTER" | grep -qP 'backend:\s*(true|false)'; then
|
||||
if echo "$FRONT_MATTER" | grep -qE 'backend:[[:space:]]*(true|false)'; then
|
||||
pass "E3 $BRIEF targets.backend is boolean"
|
||||
else
|
||||
fail "E3 $BRIEF targets.backend must be true or false"
|
||||
fi
|
||||
if echo "$FRONT_MATTER" | grep -qP 'frontend:\s*(true|false)'; then
|
||||
if echo "$FRONT_MATTER" | grep -qE 'frontend:[[:space:]]*(true|false)'; then
|
||||
pass "E3 $BRIEF targets.frontend is boolean"
|
||||
else
|
||||
fail "E3 $BRIEF targets.frontend must be true or false"
|
||||
@@ -442,8 +442,8 @@ if [[ -f "$SETTINGS" ]]; then
|
||||
fi
|
||||
done
|
||||
|
||||
# F6: hooks 配置存在
|
||||
HOOK_EVENTS=("PreToolUse" "PostToolUse" "Stop" "SessionStart")
|
||||
# F6: hooks 配置存在 (SessionStart is not a Claude Code hook type — skip)
|
||||
HOOK_EVENTS=("PreToolUse" "PostToolUse" "Stop")
|
||||
for event in "${HOOK_EVENTS[@]}"; do
|
||||
if jq -e ".hooks.${event}" "$SETTINGS" > /dev/null 2>&1; then
|
||||
pass "F6 hook configured: $event"
|
||||
@@ -642,13 +642,13 @@ if [[ -f "spec/01-constraints.md" ]]; then
|
||||
# 从 constraints 提取版本号
|
||||
declare -A VERSIONS
|
||||
VERSIONS=(
|
||||
["spring_boot"]="$(grep -oP 'Spring Boot[^0-9]*\K[0-9]+\.[0-9]+\.[0-9]+' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
["vue"]="$(grep -oP 'Vue[^0-9]*~?\K[0-9]+\.[0-9]+\.[0-9]+' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
["vite"]="$(grep -oP 'Vite[^0-9]*~?\K[0-9]+\.[0-9]+\.[0-9]+' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
["typescript"]="$(grep -oP 'TypeScript[^0-9]*~?\K[0-9]+\.[0-9]+\.[0-9]+' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
["node"]="$(grep -oP 'Node[^0-9]*\K[0-9]+\.[0-9]+\.[0-9]+' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
["pnpm"]="$(grep -oP 'pnpm[^0-9]*\K[0-9]+\.[0-9]+\.[0-9]+' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
["postgres"]="$(grep -oP 'PostgreSQL[^0-9]*\K[0-9]+\.[0-9]+' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
["spring_boot"]="$(sed -n 's/.*Spring Boot[^0-9]*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
["vue"]="$(sed -n 's/.*Vue[^0-9]*~*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
["vite"]="$(sed -n 's/.*Vite[^0-9]*~*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
["typescript"]="$(sed -n 's/.*TypeScript[^0-9]*~*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
["node"]="$(sed -n 's/.*Node[^0-9]*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
["pnpm"]="$(sed -n 's/.*pnpm[^0-9]*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
["postgres"]="$(sed -n 's/.*PostgreSQL[^0-9]*\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' spec/01-constraints.md 2>/dev/null | head -1)"
|
||||
)
|
||||
|
||||
for name in "${!VERSIONS[@]}"; do
|
||||
@@ -668,13 +668,13 @@ if [[ -f "spec/01-constraints.md" ]]; then
|
||||
|
||||
case "$name" in
|
||||
"spring_boot")
|
||||
CLAUDE_VER=$(grep -oP 'Spring Boot[^0-9]*\K[0-9]+\.[0-9]+\.[0-9]+' CLAUDE.md 2>/dev/null | head -1)
|
||||
CLAUDE_VER=$(sed -n 's/.*Spring Boot[^0-9]*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' CLAUDE.md 2>/dev/null | head -1)
|
||||
;;
|
||||
"vue")
|
||||
CLAUDE_VER=$(grep -oP 'Vue[^0-9]*~?\K[0-9]+\.[0-9]+\.[0-9]+' CLAUDE.md 2>/dev/null | head -1)
|
||||
CLAUDE_VER=$(sed -n 's/.*Vue[^0-9]*~*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' CLAUDE.md 2>/dev/null | head -1)
|
||||
;;
|
||||
"node")
|
||||
CLAUDE_VER=$(grep -oP 'Node[^0-9]*\K[0-9]+\.[0-9]+\.[0-9]+' CLAUDE.md 2>/dev/null | head -1)
|
||||
CLAUDE_VER=$(sed -n 's/.*Node[^0-9]*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' CLAUDE.md 2>/dev/null | head -1)
|
||||
;;
|
||||
*)
|
||||
CLAUDE_VER=""
|
||||
@@ -752,6 +752,7 @@ fi
|
||||
if [[ -f "spec/INDEX.md" ]]; then
|
||||
for sf in "${SPEC_FILES[@]}"; do
|
||||
BASENAME=$(basename "$sf")
|
||||
[[ "$BASENAME" == "INDEX.md" ]] && continue # index doesn't list itself
|
||||
if grep -q "$BASENAME" spec/INDEX.md; then
|
||||
pass "J3 INDEX.md lists: $BASENAME"
|
||||
else
|
||||
@@ -879,7 +880,13 @@ fi
|
||||
if echo "test" | grep -P 'test' > /dev/null 2>&1; then
|
||||
pass "K9 grep -P (PCRE) supported"
|
||||
else
|
||||
fail "K9 grep -P NOT supported — gate-rules.sh requires PCRE. Install GNU grep."
|
||||
# Check if any gate/hook scripts still use grep -P (they should not on macOS)
|
||||
PCRE_HITS=$(grep -rlF 'grep -P' scripts/gates/ scripts/hooks/ 2>/dev/null | tr '\n' ' ' || true)
|
||||
if [[ -n "$PCRE_HITS" ]]; then
|
||||
fail "K9 grep -P not supported, but scripts still use PCRE: $PCRE_HITS"
|
||||
else
|
||||
pass "K9 grep -P not available, but all scripts use ERE (macOS compatible)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# K10: sed 基本功能
|
||||
|
||||
Executable
+162
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env bash
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# run-all.sh — 全量执行器(Phase 0–27,幂等,可中断续跑)
|
||||
#
|
||||
# 用法:
|
||||
# bash scripts/run-all.sh # 执行所有未完成的 Phase
|
||||
# bash scripts/run-all.sh --from 5 # 从 Phase 5 开始
|
||||
# bash scripts/run-all.sh --only 3 # 只执行 Phase 3
|
||||
# bash scripts/run-all.sh --dry-run # 显示执行计划但不执行
|
||||
#
|
||||
# 幂等性: 检查 PHASE_MANIFEST.txt 中已完成的 Phase,跳过重复执行
|
||||
# 追溯性: 每个 Phase 的日志保存在 logs/phase-NN/
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
export CLAUDE_PROJECT_DIR="$PROJECT_ROOT"
|
||||
|
||||
# ── Argument parsing ───────────────────────────────────────
|
||||
FROM_PHASE=0
|
||||
ONLY_PHASE=""
|
||||
DRY_RUN=false
|
||||
TOTAL_PHASES=28 # Phase 0–27
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--from) FROM_PHASE="$2"; shift 2 ;;
|
||||
--only) ONLY_PHASE="$2"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
*) echo "Unknown arg: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Setup ──────────────────────────────────────────────────
|
||||
mkdir -p logs
|
||||
MANIFEST="PHASE_MANIFEST.txt"
|
||||
FAILED_LOG="logs/failed.log"
|
||||
RUN_LOG="logs/run-all-$(date +%Y%m%d-%H%M%S).log"
|
||||
|
||||
: > "$FAILED_LOG"
|
||||
|
||||
# ── Helper functions ───────────────────────────────────────
|
||||
is_completed() {
|
||||
local phase="$1"
|
||||
[[ -f "$MANIFEST" ]] && grep -q "^## PHASE $phase$" "$MANIFEST" && \
|
||||
grep -A5 "^## PHASE $phase$" "$MANIFEST" | grep -q "Status: PASSED"
|
||||
}
|
||||
|
||||
phase_exists() {
|
||||
local phase="$1"
|
||||
local padded
|
||||
padded=$(printf '%02d' "$phase")
|
||||
[[ -f "spec/phase/phase-${padded}.md" ]]
|
||||
}
|
||||
|
||||
# ── Pre-flight ─────────────────────────────────────────────
|
||||
echo "════════════════════════════════════════════════════"
|
||||
echo " LJWX Platform — Full Generation Run"
|
||||
echo " Start: $(date)"
|
||||
echo " Phases: $FROM_PHASE – $((TOTAL_PHASES - 1))"
|
||||
[[ -n "$ONLY_PHASE" ]] && echo " Mode: only Phase $ONLY_PHASE"
|
||||
$DRY_RUN && echo " Mode: DRY RUN"
|
||||
echo "════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
if ! $DRY_RUN; then
|
||||
echo "--- Preflight Check ---"
|
||||
bash scripts/preflight/preflight-check.sh
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── Determine phase list ───────────────────────────────────
|
||||
if [[ -n "$ONLY_PHASE" ]]; then
|
||||
PHASE_LIST=("$ONLY_PHASE")
|
||||
else
|
||||
PHASE_LIST=()
|
||||
for i in $(seq "$FROM_PHASE" $((TOTAL_PHASES - 1))); do
|
||||
if phase_exists "$i"; then
|
||||
PHASE_LIST+=("$i")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ── Execution plan ─────────────────────────────────────────
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
SKIPPED=0
|
||||
|
||||
echo "Execution plan (${#PHASE_LIST[@]} phases):"
|
||||
for phase in "${PHASE_LIST[@]}"; do
|
||||
padded=$(printf '%02d' "$phase")
|
||||
brief="spec/phase/phase-${padded}.md"
|
||||
title=$(grep -m1 '^title:' "$brief" 2>/dev/null | sed 's/title:[[:space:]]*//' | tr -d '"' || echo "?")
|
||||
if is_completed "$phase"; then
|
||||
echo " Phase $phase — $title [ALREADY DONE — will skip]"
|
||||
else
|
||||
echo " Phase $phase — $title"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
$DRY_RUN && echo "Dry run complete." && exit 0
|
||||
|
||||
# ── Main execution loop ────────────────────────────────────
|
||||
for phase in "${PHASE_LIST[@]}"; do
|
||||
padded=$(printf '%02d' "$phase")
|
||||
|
||||
echo ""
|
||||
echo ">>>>>>>>>> Phase $phase <<<<<<<<<<"
|
||||
|
||||
# Idempotency check
|
||||
if is_completed "$phase"; then
|
||||
echo " Phase $phase already completed — skipping."
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
PASSED=$((PASSED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Detect parallel-eligible phase (both backend and frontend)
|
||||
PHASE_BRIEF="spec/phase/phase-${padded}.md"
|
||||
YAML_BLOCK=$(awk '/^---$/{n++;if(n==1){next};if(n==2){exit}} n==1{print}' "$PHASE_BRIEF")
|
||||
T_BE=$(echo "$YAML_BLOCK" | grep 'backend:' | awk '{print $2}' | head -1)
|
||||
T_FE=$(echo "$YAML_BLOCK" | grep 'frontend:' | awk '{print $2}' | head -1)
|
||||
|
||||
EXECUTOR="scripts/phase-execute.sh"
|
||||
if [[ "${T_BE:-false}" == "true" && "${T_FE:-false}" == "true" ]]; then
|
||||
EXECUTOR="scripts/phase-parallel.sh"
|
||||
fi
|
||||
|
||||
PHASE_LOG="$LOG_DIR_BASE/phase-${padded}/run.log" 2>/dev/null || true
|
||||
LOG_BASE="logs/phase-${padded}"
|
||||
mkdir -p "$LOG_BASE"
|
||||
|
||||
# Execute
|
||||
if bash "$EXECUTOR" "$phase" "--skip-preflight" 2>&1 | tee "$LOG_BASE/run.log"; then
|
||||
echo "Phase $phase — PASSED"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
EXIT_CODE=$?
|
||||
echo "Phase $phase — FAILED (exit $EXIT_CODE)"
|
||||
echo "Phase $phase FAILED — $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$FAILED_LOG"
|
||||
FAILED=$((FAILED + 1))
|
||||
echo "Continuing to next phase..."
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Summary ────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════"
|
||||
echo " LJWX Platform — Generation Complete"
|
||||
echo " End: $(date)"
|
||||
echo " Total: ${#PHASE_LIST[@]} | Passed: $PASSED | Failed: $FAILED | Skipped: $SKIPPED"
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
echo ""
|
||||
echo " FAILED phases:"
|
||||
cat "$FAILED_LOG"
|
||||
fi
|
||||
echo " Logs: logs/"
|
||||
echo "════════════════════════════════════════════════════"
|
||||
|
||||
exit "$FAILED"
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# Export OpenAPI spec from a running application instance.
|
||||
# Usage: bash scripts/tools/export-openapi.sh [base-url]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-http://localhost:8080}"
|
||||
OUTPUT_DIR="docs/contracts"
|
||||
OUTPUT="${OUTPUT_DIR}/openapi.json"
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
echo "[export-openapi] Fetching spec from ${BASE_URL}/v3/api-docs"
|
||||
|
||||
for i in $(seq 1 10); do
|
||||
if curl -sf "${BASE_URL}/v3/api-docs" -o "$OUTPUT"; then
|
||||
echo "[export-openapi] Saved to ${OUTPUT}"
|
||||
exit 0
|
||||
fi
|
||||
echo " Attempt $i failed, retrying in 2s..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "[export-openapi] FAIL: Could not fetch OpenAPI spec from ${BASE_URL}/v3/api-docs"
|
||||
exit 1
|
||||
Reference in New Issue
Block a user