adspecs 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.adspecs/feature.json +16 -16
  2. package/.adspecs/feature.yml +28 -28
  3. package/.adspecs/paths.json +17 -17
  4. package/.claude-plugin/marketplace.json +23 -23
  5. package/.claude-plugin/plugin.json +18 -18
  6. package/.qoder-plugin/plugin.json +31 -31
  7. package/CLAUDE.md +1 -5
  8. package/README.md +395 -395
  9. package/bin/adspecs.js +129 -129
  10. package/hooks/commit-queue.js +245 -245
  11. package/hooks/hooks.json +63 -63
  12. package/hooks/session-start.js +44 -44
  13. package/hooks/wiki-queue.js +127 -127
  14. package/package.json +61 -61
  15. package/references/ant6-front-standard/index.md +99 -99
  16. package/references/antd-front-demo/public/mockServiceWorker.js +361 -361
  17. package/references/ecp-end-standard/index.md +63 -63
  18. package/references/python-end-standard/01-Python/345/220/216/347/253/257/347/274/226/347/240/201/350/247/204/350/214/203.md +372 -372
  19. package/references/python-end-standard/02-/346/225/260/346/215/256/345/272/223/350/256/276/350/256/241/344/270/216/344/275/277/347/224/250/350/247/204/350/214/203.md +226 -226
  20. package/references/python-end-standard/03-Celery/345/274/202/346/255/245/344/273/273/345/212/241/350/247/204/350/214/203.md +237 -237
  21. package/references/python-end-standard/04-Redis/344/275/277/347/224/250/350/247/204/350/214/203.md +231 -231
  22. package/scripts/postinstall.js +107 -107
  23. package/scripts/sync-version.js +105 -105
  24. package/skills/.claude/.wiki-update-queue +26 -26
  25. package/skills/adspecs-constitution/SKILL.md +157 -0
  26. package/skills/adspecs-export-word/SKILL.md +498 -498
  27. package/skills/adspecs-export-word/references/md-to-docx.js +862 -862
  28. package/skills/adspecs-export-word/references/package-lock.json +220 -220
  29. package/skills/adspecs-export-word/references/package.json +10 -10
  30. package/skills/adspecs-front-prototype/SKILL.md +405 -405
  31. package/skills/adspecs-plan/SKILL.md +59 -69
  32. package/skills/adspecs-prd/SKILL.md +13 -5
  33. package/skills/adspecs-prd-to-demo/SKILL.md +532 -0
  34. package/skills/adspecs-tasks/SKILL.md +175 -204
  35. package/skills/adspecs-update-status/SKILL.md +382 -382
  36. package/skills/adspecs-utest/SKILL.md +107 -116
  37. package/skills/grill-me/SKILL.md +7 -0
  38. package/skills/grill-me/agents/openai.yaml +5 -0
  39. package/skills/playwright-cli/SKILL.md +420 -0
  40. package/skills/playwright-cli/references/element-attributes.md +23 -0
  41. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  42. package/skills/playwright-cli/references/request-mocking.md +87 -0
  43. package/skills/playwright-cli/references/running-code.md +241 -0
  44. package/skills/playwright-cli/references/session-management.md +225 -0
  45. package/skills/playwright-cli/references/storage-state.md +275 -0
  46. package/skills/playwright-cli/references/test-generation.md +433 -0
  47. package/skills/playwright-cli/references/tracing.md +139 -0
  48. package/skills/playwright-cli/references/video-recording.md +143 -0
  49. package/skills/playwright-trace/SKILL.md +171 -0
  50. package/skills/project-init/references/front-demo/.claude/settings.local.json +9 -0
  51. package/skills/wiki-update/SKILL.md +232 -232
  52. package/src/commands/doctor.js +197 -197
  53. package/src/commands/init.js +83 -83
  54. package/src/commands/plugin.js +165 -165
  55. package/src/commands/update.js +87 -87
  56. package/src/lib/area-scanner.js +129 -129
  57. package/src/lib/copier.js +104 -104
  58. package/src/lib/dir-utils.js +133 -133
  59. package/src/lib/json-merge.js +114 -114
  60. package/src/lib/paths-defaults.js +37 -37
  61. package/src/lib/prompts.js +347 -347
  62. package/src/lib/readme-gen.js +143 -143
  63. package/src/lib/report.js +327 -327
  64. package/src/lib/scaffolder.js +518 -518
  65. package/src/lib/short-name.js +36 -36
  66. package/src/utils.js +80 -80
  67. package/references/antd-front-demo/.env +0 -15
@@ -1,226 +1,226 @@
1
- # 数据库设计与使用规范
2
-
3
- > **版本**: v1.0 | **修订日期**: 2026-07-15
4
- > **技术栈**: SQLAlchemy 2.0+ / Alembic / SQLite (aiosqlite)
5
-
6
- ---
7
-
8
- ## 一、SQLAlchemy 基类
9
-
10
- ### 1.1 声明式基类
11
-
12
- ```python
13
- # app/models/base.py
14
- from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
15
- from sqlalchemy import Integer, String, Boolean, DateTime, func
16
- from datetime import datetime
17
-
18
- class Base(DeclarativeBase):
19
- """SQLAlchemy 声明式基类"""
20
- pass
21
-
22
- class TimestampMixin:
23
- """时间戳 Mixin — 自动记录创建/更新时间"""
24
- created_at: Mapped[datetime] = mapped_column(
25
- DateTime, server_default=func.now(), nullable=False
26
- )
27
- updated_at: Mapped[datetime] = mapped_column(
28
- DateTime, server_default=func.now(), onupdate=func.now(), nullable=False
29
- )
30
-
31
- class SoftDeleteMixin:
32
- """软删除 Mixin"""
33
- is_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
34
-
35
- class AuditMixin:
36
- """审计 Mixin — 记录操作人"""
37
- created_by: Mapped[int | None] = mapped_column(Integer, nullable=True)
38
- updated_by: Mapped[int | None] = mapped_column(Integer, nullable=True)
39
- ```
40
-
41
- ### 1.2 模型定义示例
42
-
43
- ```python
44
- # app/models/warehouse.py
45
- from sqlalchemy import String, Integer, Text, Index
46
- from sqlalchemy.orm import Mapped, mapped_column, relationship
47
- from app.models.base import Base, TimestampMixin, SoftDeleteMixin, AuditMixin
48
-
49
- class Warehouse(Base, TimestampMixin, SoftDeleteMixin, AuditMixin):
50
- """仓库模型"""
51
- __tablename__ = "warehouses"
52
- __table_args__ = (
53
- Index("ix_warehouse_code", "code", unique=True),
54
- Index("ix_warehouse_active", "is_active"),
55
- )
56
-
57
- id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
58
- code: Mapped[str] = mapped_column(String(50), nullable=False, comment="仓库编码")
59
- name: Mapped[str] = mapped_column(String(200), nullable=False, comment="仓库名称")
60
- location: Mapped[str | None] = mapped_column(String(500), comment="位置")
61
- is_active: Mapped[bool] = mapped_column(default=True, comment="是否启用")
62
-
63
- # relationships
64
- material_lots: Mapped[list["MaterialLot"]] = relationship(back_populates="warehouse")
65
- ```
66
-
67
- ---
68
-
69
- ## 二、SQLite 特有注意事项
70
-
71
- | 事项 | 规范 |
72
- |----------------|------------------------------------------------------------|
73
- | WAL 模式 | 生产环境必须启用 `PRAGMA journal_mode=WAL` |
74
- | 外键约束 | 默认关闭,必须执行 `PRAGMA foreign_keys=ON` |
75
- | 类型亲和性 | 严格按模型定义列类型,避免隐式转换 |
76
- | 并发写入 | SQLite 不支持并发写,Celery worker 必须 `--concurrency=1` |
77
- | 文件权限 | 数据库文件目录需要读写权限 |
78
- | 备份 | 定期 VACUUM + 文件拷贝 |
79
-
80
- ### 引擎配置
81
-
82
- ```python
83
- # app/core/database.py
84
- from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
85
- from sqlalchemy import text
86
- from app.core.config import settings
87
-
88
- engine = create_async_engine(
89
- settings.DATABASE_URL,
90
- echo=settings.DEBUG,
91
- connect_args={"check_same_thread": False},
92
- )
93
-
94
- async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
95
-
96
- async def init_db():
97
- """初始化数据库(启用 WAL 模式)"""
98
- async with engine.connect() as conn:
99
- await conn.execute(text("PRAGMA journal_mode=WAL"))
100
- await conn.execute(text("PRAGMA foreign_keys=ON"))
101
- await conn.commit()
102
-
103
- async def get_db():
104
- """数据库会话依赖注入"""
105
- async with async_session() as session:
106
- try:
107
- yield session
108
- except Exception:
109
- await session.rollback()
110
- raise
111
- ```
112
-
113
- ---
114
-
115
- ## 三、Alembic 迁移规范
116
-
117
- ### 3.1 初始化
118
-
119
- ```bash
120
- alembic init alembic
121
- # 修改 alembic.ini 的 sqlalchemy.url
122
- # 修改 alembic/env.py 导入 Base.metadata
123
- ```
124
-
125
- ### 3.2 迁移命名
126
-
127
- ```bash
128
- alembic revision --autogenerate -m "add_warehouse_table"
129
- alembic revision --autogenerate -m "add_index_warehouse_code"
130
- ```
131
-
132
- ### 3.3 迁移规则
133
-
134
- | 规则 | 说明 |
135
- |------------|------------------------------------------------|
136
- | 自动生成 | 始终使用 `--autogenerate`,手动 review 后提交 |
137
- | 不可逆操作 | DROP TABLE/DROP COLUMN 前必须确认数据安全 |
138
- | 数据迁移 | 数据迁移和结构迁移分开为两个 revision |
139
- | 测试 | 迁移后必须运行测试验证 |
140
- | 提交 | 迁移文件必须纳入版本控制 |
141
-
142
- ---
143
-
144
- ## 四、查询规范
145
-
146
- ### 4.1 使用 select() 风格
147
-
148
- ```python
149
- from sqlalchemy import select, func
150
-
151
- # 单条查询
152
- stmt = select(Warehouse).where(Warehouse.id == warehouse_id)
153
- result = await db.execute(stmt)
154
- warehouse = result.scalar_one_or_none()
155
-
156
- # 分页查询
157
- stmt = (
158
- select(Warehouse)
159
- .where(Warehouse.is_deleted == False, Warehouse.is_active == True)
160
- .order_by(Warehouse.created_at.desc())
161
- .offset((page - 1) * page_size)
162
- .limit(page_size)
163
- )
164
- result = await db.execute(stmt)
165
- items = result.scalars().all()
166
-
167
- # 计数
168
- count_stmt = select(func.count()).select_from(Warehouse).where(Warehouse.is_deleted == False)
169
- total = (await db.execute(count_stmt)).scalar()
170
- ```
171
-
172
- ### 4.2 禁止事项
173
-
174
- - 禁止使用 `session.query()` 风格(SQLAlchemy 1.x 遗留)
175
- - 禁止字符串拼接 SQL
176
- - 禁止在循环中执行查询(N+1 问题)
177
- - 禁止不带 WHERE 的 UPDATE/DELETE
178
-
179
- ---
180
-
181
- ## 五、Repository 模式
182
-
183
- ```python
184
- # app/repositories/warehouse.py
185
- class WarehouseRepository:
186
- """仓库数据访问层"""
187
-
188
- async def get_by_id(self, db: AsyncSession, warehouse_id: int) -> Warehouse | None:
189
- return await db.get(Warehouse, warehouse_id)
190
-
191
- async def get_by_code(self, db: AsyncSession, code: str) -> Warehouse | None:
192
- stmt = select(Warehouse).where(Warehouse.code == code, Warehouse.is_deleted == False)
193
- result = await db.execute(stmt)
194
- return result.scalar_one_or_none()
195
-
196
- async def get_page(
197
- self, db: AsyncSession, page: int, page_size: int, filters: dict | None = None
198
- ) -> tuple[list[Warehouse], int]:
199
- base = select(Warehouse).where(Warehouse.is_deleted == False)
200
- if filters:
201
- if "name" in filters:
202
- base = base.where(Warehouse.name.contains(filters["name"]))
203
- if "is_active" in filters:
204
- base = base.where(Warehouse.is_active == filters["is_active"])
205
-
206
- count_stmt = select(func.count()).select_from(base.subquery())
207
- total = (await db.execute(count_stmt)).scalar()
208
-
209
- stmt = base.order_by(Warehouse.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
210
- result = await db.execute(stmt)
211
- return result.scalars().all(), total
212
-
213
- warehouse_repo = WarehouseRepository()
214
- ```
215
-
216
- ---
217
-
218
- ## 六、索引策略
219
-
220
- | 场景 | 索引类型 | 示例 |
221
- |----------------|------------------|-------------------------------------------|
222
- | 唯一约束 | Unique Index | code, email |
223
- | 高频查询条件 | 普通索引 | is_active, status |
224
- | 组合查询 | 复合索引 | (warehouse_id, material_type) |
225
- | 外键 | 自动创建 | relationship 自动生成 |
226
- | 软删除过滤 | 部分索引 | WHERE is_deleted = False |
1
+ # 数据库设计与使用规范
2
+
3
+ > **版本**: v1.0 | **修订日期**: 2026-07-15
4
+ > **技术栈**: SQLAlchemy 2.0+ / Alembic / SQLite (aiosqlite)
5
+
6
+ ---
7
+
8
+ ## 一、SQLAlchemy 基类
9
+
10
+ ### 1.1 声明式基类
11
+
12
+ ```python
13
+ # app/models/base.py
14
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
15
+ from sqlalchemy import Integer, String, Boolean, DateTime, func
16
+ from datetime import datetime
17
+
18
+ class Base(DeclarativeBase):
19
+ """SQLAlchemy 声明式基类"""
20
+ pass
21
+
22
+ class TimestampMixin:
23
+ """时间戳 Mixin — 自动记录创建/更新时间"""
24
+ created_at: Mapped[datetime] = mapped_column(
25
+ DateTime, server_default=func.now(), nullable=False
26
+ )
27
+ updated_at: Mapped[datetime] = mapped_column(
28
+ DateTime, server_default=func.now(), onupdate=func.now(), nullable=False
29
+ )
30
+
31
+ class SoftDeleteMixin:
32
+ """软删除 Mixin"""
33
+ is_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
34
+
35
+ class AuditMixin:
36
+ """审计 Mixin — 记录操作人"""
37
+ created_by: Mapped[int | None] = mapped_column(Integer, nullable=True)
38
+ updated_by: Mapped[int | None] = mapped_column(Integer, nullable=True)
39
+ ```
40
+
41
+ ### 1.2 模型定义示例
42
+
43
+ ```python
44
+ # app/models/warehouse.py
45
+ from sqlalchemy import String, Integer, Text, Index
46
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
47
+ from app.models.base import Base, TimestampMixin, SoftDeleteMixin, AuditMixin
48
+
49
+ class Warehouse(Base, TimestampMixin, SoftDeleteMixin, AuditMixin):
50
+ """仓库模型"""
51
+ __tablename__ = "warehouses"
52
+ __table_args__ = (
53
+ Index("ix_warehouse_code", "code", unique=True),
54
+ Index("ix_warehouse_active", "is_active"),
55
+ )
56
+
57
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
58
+ code: Mapped[str] = mapped_column(String(50), nullable=False, comment="仓库编码")
59
+ name: Mapped[str] = mapped_column(String(200), nullable=False, comment="仓库名称")
60
+ location: Mapped[str | None] = mapped_column(String(500), comment="位置")
61
+ is_active: Mapped[bool] = mapped_column(default=True, comment="是否启用")
62
+
63
+ # relationships
64
+ material_lots: Mapped[list["MaterialLot"]] = relationship(back_populates="warehouse")
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 二、SQLite 特有注意事项
70
+
71
+ | 事项 | 规范 |
72
+ |----------------|------------------------------------------------------------|
73
+ | WAL 模式 | 生产环境必须启用 `PRAGMA journal_mode=WAL` |
74
+ | 外键约束 | 默认关闭,必须执行 `PRAGMA foreign_keys=ON` |
75
+ | 类型亲和性 | 严格按模型定义列类型,避免隐式转换 |
76
+ | 并发写入 | SQLite 不支持并发写,Celery worker 必须 `--concurrency=1` |
77
+ | 文件权限 | 数据库文件目录需要读写权限 |
78
+ | 备份 | 定期 VACUUM + 文件拷贝 |
79
+
80
+ ### 引擎配置
81
+
82
+ ```python
83
+ # app/core/database.py
84
+ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
85
+ from sqlalchemy import text
86
+ from app.core.config import settings
87
+
88
+ engine = create_async_engine(
89
+ settings.DATABASE_URL,
90
+ echo=settings.DEBUG,
91
+ connect_args={"check_same_thread": False},
92
+ )
93
+
94
+ async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
95
+
96
+ async def init_db():
97
+ """初始化数据库(启用 WAL 模式)"""
98
+ async with engine.connect() as conn:
99
+ await conn.execute(text("PRAGMA journal_mode=WAL"))
100
+ await conn.execute(text("PRAGMA foreign_keys=ON"))
101
+ await conn.commit()
102
+
103
+ async def get_db():
104
+ """数据库会话依赖注入"""
105
+ async with async_session() as session:
106
+ try:
107
+ yield session
108
+ except Exception:
109
+ await session.rollback()
110
+ raise
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 三、Alembic 迁移规范
116
+
117
+ ### 3.1 初始化
118
+
119
+ ```bash
120
+ alembic init alembic
121
+ # 修改 alembic.ini 的 sqlalchemy.url
122
+ # 修改 alembic/env.py 导入 Base.metadata
123
+ ```
124
+
125
+ ### 3.2 迁移命名
126
+
127
+ ```bash
128
+ alembic revision --autogenerate -m "add_warehouse_table"
129
+ alembic revision --autogenerate -m "add_index_warehouse_code"
130
+ ```
131
+
132
+ ### 3.3 迁移规则
133
+
134
+ | 规则 | 说明 |
135
+ |------------|------------------------------------------------|
136
+ | 自动生成 | 始终使用 `--autogenerate`,手动 review 后提交 |
137
+ | 不可逆操作 | DROP TABLE/DROP COLUMN 前必须确认数据安全 |
138
+ | 数据迁移 | 数据迁移和结构迁移分开为两个 revision |
139
+ | 测试 | 迁移后必须运行测试验证 |
140
+ | 提交 | 迁移文件必须纳入版本控制 |
141
+
142
+ ---
143
+
144
+ ## 四、查询规范
145
+
146
+ ### 4.1 使用 select() 风格
147
+
148
+ ```python
149
+ from sqlalchemy import select, func
150
+
151
+ # 单条查询
152
+ stmt = select(Warehouse).where(Warehouse.id == warehouse_id)
153
+ result = await db.execute(stmt)
154
+ warehouse = result.scalar_one_or_none()
155
+
156
+ # 分页查询
157
+ stmt = (
158
+ select(Warehouse)
159
+ .where(Warehouse.is_deleted == False, Warehouse.is_active == True)
160
+ .order_by(Warehouse.created_at.desc())
161
+ .offset((page - 1) * page_size)
162
+ .limit(page_size)
163
+ )
164
+ result = await db.execute(stmt)
165
+ items = result.scalars().all()
166
+
167
+ # 计数
168
+ count_stmt = select(func.count()).select_from(Warehouse).where(Warehouse.is_deleted == False)
169
+ total = (await db.execute(count_stmt)).scalar()
170
+ ```
171
+
172
+ ### 4.2 禁止事项
173
+
174
+ - 禁止使用 `session.query()` 风格(SQLAlchemy 1.x 遗留)
175
+ - 禁止字符串拼接 SQL
176
+ - 禁止在循环中执行查询(N+1 问题)
177
+ - 禁止不带 WHERE 的 UPDATE/DELETE
178
+
179
+ ---
180
+
181
+ ## 五、Repository 模式
182
+
183
+ ```python
184
+ # app/repositories/warehouse.py
185
+ class WarehouseRepository:
186
+ """仓库数据访问层"""
187
+
188
+ async def get_by_id(self, db: AsyncSession, warehouse_id: int) -> Warehouse | None:
189
+ return await db.get(Warehouse, warehouse_id)
190
+
191
+ async def get_by_code(self, db: AsyncSession, code: str) -> Warehouse | None:
192
+ stmt = select(Warehouse).where(Warehouse.code == code, Warehouse.is_deleted == False)
193
+ result = await db.execute(stmt)
194
+ return result.scalar_one_or_none()
195
+
196
+ async def get_page(
197
+ self, db: AsyncSession, page: int, page_size: int, filters: dict | None = None
198
+ ) -> tuple[list[Warehouse], int]:
199
+ base = select(Warehouse).where(Warehouse.is_deleted == False)
200
+ if filters:
201
+ if "name" in filters:
202
+ base = base.where(Warehouse.name.contains(filters["name"]))
203
+ if "is_active" in filters:
204
+ base = base.where(Warehouse.is_active == filters["is_active"])
205
+
206
+ count_stmt = select(func.count()).select_from(base.subquery())
207
+ total = (await db.execute(count_stmt)).scalar()
208
+
209
+ stmt = base.order_by(Warehouse.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
210
+ result = await db.execute(stmt)
211
+ return result.scalars().all(), total
212
+
213
+ warehouse_repo = WarehouseRepository()
214
+ ```
215
+
216
+ ---
217
+
218
+ ## 六、索引策略
219
+
220
+ | 场景 | 索引类型 | 示例 |
221
+ |----------------|------------------|-------------------------------------------|
222
+ | 唯一约束 | Unique Index | code, email |
223
+ | 高频查询条件 | 普通索引 | is_active, status |
224
+ | 组合查询 | 复合索引 | (warehouse_id, material_type) |
225
+ | 外键 | 自动创建 | relationship 自动生成 |
226
+ | 软删除过滤 | 部分索引 | WHERE is_deleted = False |