loom-memory-mcp 0.2.1__tar.gz
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.
- loom_memory_mcp-0.2.1/.gitignore +41 -0
- loom_memory_mcp-0.2.1/PKG-INFO +63 -0
- loom_memory_mcp-0.2.1/README.md +38 -0
- loom_memory_mcp-0.2.1/_mycomp_test.ts +3 -0
- loom_memory_mcp-0.2.1/_paths.py +34 -0
- loom_memory_mcp-0.2.1/analyze_repo.py +194 -0
- loom_memory_mcp-0.2.1/backfill_tradeoffs.py +101 -0
- loom_memory_mcp-0.2.1/bench_remote.py +66 -0
- loom_memory_mcp-0.2.1/bench_report.py +45 -0
- loom_memory_mcp-0.2.1/demo_saas.py +106 -0
- loom_memory_mcp-0.2.1/embedding.py +154 -0
- loom_memory_mcp-0.2.1/eval_flywheel.py +88 -0
- loom_memory_mcp-0.2.1/eval_mcp_e2e.py +51 -0
- loom_memory_mcp-0.2.1/eval_pool_density.py +77 -0
- loom_memory_mcp-0.2.1/eval_propose.py +45 -0
- loom_memory_mcp-0.2.1/eval_retrieval.py +77 -0
- loom_memory_mcp-0.2.1/eval_writeown.py +76 -0
- loom_memory_mcp-0.2.1/export_schemas.py +41 -0
- loom_memory_mcp-0.2.1/flywheel.py +168 -0
- loom_memory_mcp-0.2.1/gen_seed.py +60 -0
- loom_memory_mcp-0.2.1/get_files.py +404 -0
- loom_memory_mcp-0.2.1/honest_report.py +51 -0
- loom_memory_mcp-0.2.1/infer_seam.py +118 -0
- loom_memory_mcp-0.2.1/ingest.py +166 -0
- loom_memory_mcp-0.2.1/load_candidates.py +63 -0
- loom_memory_mcp-0.2.1/loom_contracts.py +450 -0
- loom_memory_mcp-0.2.1/loom_entry.py +36 -0
- loom_memory_mcp-0.2.1/loom_seed/loom.core.json +171 -0
- loom_memory_mcp-0.2.1/loom_seed/seed_data.json +502 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/.env.example +24 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/.gitignore +46 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/README.md +29 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/next-env.d.ts +5 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/next.config.js +10 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/package.json +48 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/pnpm-lock.yaml +1577 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/pnpm-workspace.yaml +5 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/postcss.config.js +5 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/prisma/schema.prisma +77 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/public/favicon.ico +0 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/app/_components/post.tsx +50 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/app/api/auth/[...nextauth]/route.ts +3 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/app/api/trpc/[trpc]/route.ts +34 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/app/layout.tsx +29 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/app/page.tsx +69 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/env.js +54 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/server/api/root.ts +25 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/server/api/routers/post.ts +41 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/server/api/trpc.ts +133 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/server/auth/config.ts +58 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/server/auth/index.ts +10 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/server/db.ts +16 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/styles/globals.css +6 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/trpc/query-client.ts +25 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/trpc/react.tsx +78 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/src/trpc/server.ts +30 -0
- loom_memory_mcp-0.2.1/loom_seed/t3-base/tsconfig.json +42 -0
- loom_memory_mcp-0.2.1/loom_vendor/__init__.py +0 -0
- loom_memory_mcp-0.2.1/loom_vendor/fact_store.py +264 -0
- loom_memory_mcp-0.2.1/m2_verdict.py +127 -0
- loom_memory_mcp-0.2.1/mcp_server.py +353 -0
- loom_memory_mcp-0.2.1/memory_backend.py +268 -0
- loom_memory_mcp-0.2.1/plan_from_choices.py +127 -0
- loom_memory_mcp-0.2.1/propose.py +195 -0
- loom_memory_mcp-0.2.1/pyproject.toml +46 -0
- loom_memory_mcp-0.2.1/retrieve.py +92 -0
- loom_memory_mcp-0.2.1/run_compare.py +69 -0
- loom_memory_mcp-0.2.1/run_select.py +312 -0
- loom_memory_mcp-0.2.1/seed_pool.py +131 -0
- loom_memory_mcp-0.2.1/uv.lock +1584 -0
- loom_memory_mcp-0.2.1/verify_candidates.py +115 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ── 依赖(各语言各目录的 node_modules / venv)──
|
|
2
|
+
node_modules/
|
|
3
|
+
**/node_modules/
|
|
4
|
+
.venv/
|
|
5
|
+
**/.venv/
|
|
6
|
+
__pycache__/
|
|
7
|
+
**/__pycache__/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
|
|
10
|
+
# ── 物化产物 / 工作目录(运行时生成,不入库)──
|
|
11
|
+
.work/
|
|
12
|
+
**/.next/
|
|
13
|
+
**/generated/
|
|
14
|
+
*.sqlite
|
|
15
|
+
*.tsbuildinfo
|
|
16
|
+
|
|
17
|
+
# ── 环境与密钥(绝不上传)──
|
|
18
|
+
.env
|
|
19
|
+
.env.*
|
|
20
|
+
!.env.example
|
|
21
|
+
*.key
|
|
22
|
+
*-key.txt
|
|
23
|
+
secrets.*
|
|
24
|
+
|
|
25
|
+
# ── OMC / 会话 / 缓存 ──
|
|
26
|
+
.omc/
|
|
27
|
+
.claude/state/
|
|
28
|
+
.claude/worktrees/
|
|
29
|
+
|
|
30
|
+
# ── 锁文件(保留 pnpm-lock 但忽略大体积可重生成的)──
|
|
31
|
+
# pnpm-lock.yaml 保留(可复现安装)
|
|
32
|
+
|
|
33
|
+
# ── 系统 ──
|
|
34
|
+
.DS_Store
|
|
35
|
+
Thumbs.db
|
|
36
|
+
*.log
|
|
37
|
+
|
|
38
|
+
# PyInstaller 打包产物
|
|
39
|
+
platform/build/
|
|
40
|
+
platform/dist/
|
|
41
|
+
platform/*.spec
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loom-memory-mcp
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Loom MCP — AI 编程的肌肉记忆,从你自己的代码库检索-组装新项目
|
|
5
|
+
Project-URL: Homepage, https://github.com/ailiheizi/loom
|
|
6
|
+
Project-URL: Repository, https://github.com/ailiheizi/loom
|
|
7
|
+
Author: ailiheizi
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: ai-coding,code-assembly,create-t3-app,mcp,scaffolding
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Requires-Dist: anthropic>=0.40
|
|
16
|
+
Requires-Dist: faiss-cpu>=1.14.3
|
|
17
|
+
Requires-Dist: fastembed>=0.8.0
|
|
18
|
+
Requires-Dist: mcp>=1.2
|
|
19
|
+
Requires-Dist: openai>=1.0
|
|
20
|
+
Requires-Dist: pydantic>=2.0
|
|
21
|
+
Requires-Dist: tokenizers==0.22.2
|
|
22
|
+
Requires-Dist: tree-sitter-typescript>=0.23.2
|
|
23
|
+
Requires-Dist: tree-sitter>=0.25.2
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# loom-mcp
|
|
27
|
+
|
|
28
|
+
**AI 编程的肌肉记忆** —— 一个持续学习的代码组装 MCP server。
|
|
29
|
+
|
|
30
|
+
把你写过的代码自动收录为可复用组件,下次碰到类似需求,AI 直接从你自己的组件库里挑着用,越用越强。
|
|
31
|
+
|
|
32
|
+
## 安装
|
|
33
|
+
|
|
34
|
+
需要 [uv](https://docs.astral.sh/uv/)。在 Claude Code / Cursor 等 MCP agent 里配:
|
|
35
|
+
|
|
36
|
+
```jsonc
|
|
37
|
+
// .mcp.json
|
|
38
|
+
{ "mcpServers": { "loom": { "command": "uvx", "args": ["loom-mcp"] } } }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
或:`claude mcp add loom -- uvx loom-mcp`
|
|
42
|
+
|
|
43
|
+
首次运行自动在 `~/.loom/` 初始化个人组件库(内置种子候选)。
|
|
44
|
+
|
|
45
|
+
## 工具
|
|
46
|
+
|
|
47
|
+
- `loom_propose(idea_json)` — 对想法返回每个能力 seam 的 2-3 个候选 + 架构取舍
|
|
48
|
+
- `loom_plan_from_choices(idea_json, choices_json)` — 选择 → 装配计划
|
|
49
|
+
- `loom_get_files(plan_json)` — 物化成完整 create-t3-app 项目文件
|
|
50
|
+
- `loom_ingest(paths, seam_hint?, description?)` — 收录你写的代码进组件库
|
|
51
|
+
|
|
52
|
+
## 工作流
|
|
53
|
+
|
|
54
|
+
说想法 → propose 给候选梯度(agent 帮你挑,不确定才问)→ get_files 返回完整项目
|
|
55
|
+
→ 写盘 + `pnpm install` + 填 `.env` + `pnpm dev`。写完新代码 `loom_ingest` 收录,越用越强。
|
|
56
|
+
|
|
57
|
+
全程本地、零网络、零 key(检索/物化不调 LLM)。信任飞轮:常用的候选浮顶,久不用的沉底。
|
|
58
|
+
|
|
59
|
+
详见 [项目主页](https://github.com/ailiheizi/loom)。
|
|
60
|
+
|
|
61
|
+
## 许可
|
|
62
|
+
|
|
63
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# loom-mcp
|
|
2
|
+
|
|
3
|
+
**AI 编程的肌肉记忆** —— 一个持续学习的代码组装 MCP server。
|
|
4
|
+
|
|
5
|
+
把你写过的代码自动收录为可复用组件,下次碰到类似需求,AI 直接从你自己的组件库里挑着用,越用越强。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
需要 [uv](https://docs.astral.sh/uv/)。在 Claude Code / Cursor 等 MCP agent 里配:
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
// .mcp.json
|
|
13
|
+
{ "mcpServers": { "loom": { "command": "uvx", "args": ["loom-mcp"] } } }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
或:`claude mcp add loom -- uvx loom-mcp`
|
|
17
|
+
|
|
18
|
+
首次运行自动在 `~/.loom/` 初始化个人组件库(内置种子候选)。
|
|
19
|
+
|
|
20
|
+
## 工具
|
|
21
|
+
|
|
22
|
+
- `loom_propose(idea_json)` — 对想法返回每个能力 seam 的 2-3 个候选 + 架构取舍
|
|
23
|
+
- `loom_plan_from_choices(idea_json, choices_json)` — 选择 → 装配计划
|
|
24
|
+
- `loom_get_files(plan_json)` — 物化成完整 create-t3-app 项目文件
|
|
25
|
+
- `loom_ingest(paths, seam_hint?, description?)` — 收录你写的代码进组件库
|
|
26
|
+
|
|
27
|
+
## 工作流
|
|
28
|
+
|
|
29
|
+
说想法 → propose 给候选梯度(agent 帮你挑,不确定才问)→ get_files 返回完整项目
|
|
30
|
+
→ 写盘 + `pnpm install` + 填 `.env` + `pnpm dev`。写完新代码 `loom_ingest` 收录,越用越强。
|
|
31
|
+
|
|
32
|
+
全程本地、零网络、零 key(检索/物化不调 LLM)。信任飞轮:常用的候选浮顶,久不用的沉底。
|
|
33
|
+
|
|
34
|
+
详见 [项目主页](https://github.com/ailiheizi/loom)。
|
|
35
|
+
|
|
36
|
+
## 许可
|
|
37
|
+
|
|
38
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""统一路径解析:兼容"仓库内运行"和"uvx/pip 安装后运行"两种场景。
|
|
2
|
+
|
|
3
|
+
- 开发(仓库内):core.json 在 ../core/loom.core.json
|
|
4
|
+
- 发布(uvx 装):打进包的 loom_seed/loom.core.json
|
|
5
|
+
优先用包内副本(发布场景),回退仓库路径(开发场景)。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
_HERE = Path(__file__).resolve().parent # platform/
|
|
12
|
+
_REPO_ROOT = _HERE.parent # 仓库根(开发时)
|
|
13
|
+
_SEED_DIR = _HERE / "loom_seed" # 包内数据
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def core_json_path() -> Path:
|
|
17
|
+
"""loom.core.json 路径:包内优先,回退仓库。"""
|
|
18
|
+
packaged = _SEED_DIR / "loom.core.json"
|
|
19
|
+
if packaged.exists():
|
|
20
|
+
return packaged
|
|
21
|
+
return _REPO_ROOT / "core" / "loom.core.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def seed_data_path() -> Path:
|
|
25
|
+
"""seed_data.json 路径(只在包内)。"""
|
|
26
|
+
return _SEED_DIR / "seed_data.json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def base_dir() -> Path:
|
|
30
|
+
"""t3-base 源目录:包内优先(loom_seed/t3-base),回退仓库(core/t3-base)。"""
|
|
31
|
+
packaged = _SEED_DIR / "t3-base"
|
|
32
|
+
if packaged.exists():
|
|
33
|
+
return packaged
|
|
34
|
+
return _REPO_ROOT / "core" / "t3-base"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""仓库分析入口(loom.analyze_repo)—— 飞轮自举的"前人种树"入口。
|
|
2
|
+
|
|
3
|
+
输入一个真实 repo 目录 → 扫 TS/TSX 源 → tree-sitter 抽 export 签名 →
|
|
4
|
+
AI(deepseek)判断每个文件对齐到哪个 loom 接缝(或 skip)→ 对齐的走 ingest 入池
|
|
5
|
+
(provenance=user,高初始信任)。入池前每个候选过 verify_candidates 质量门(防垃圾)。
|
|
6
|
+
|
|
7
|
+
"前人种树后人乘凉":一个人贡献一个 repo,AI 自动拆成接缝级候选,
|
|
8
|
+
后来的想法就能 pick 到这些现成组件 → 池靠社区贡献自增长。
|
|
9
|
+
|
|
10
|
+
最小真实版(一轮可做完可测):
|
|
11
|
+
- 单语言 TS/TSX,对齐到现有 6 个 core 接缝
|
|
12
|
+
- AI 单轮 JSON 输出对齐决策(复用 run_select 的 deepseek 路径)
|
|
13
|
+
诚实推迟:多语言、跨文件依赖分析、新接缝自动发现、大规模 repo。
|
|
14
|
+
|
|
15
|
+
用法:
|
|
16
|
+
LOOM_LLM_PROVIDER=deepseek LOOM_LLM_API_KEY=sk-... \
|
|
17
|
+
uv run python analyze_repo.py <repo_dir> [--dry-run]
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
28
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
from ingest import extract_exports, ingest_file
|
|
31
|
+
|
|
32
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
33
|
+
CANDIDATES = ROOT / "candidates"
|
|
34
|
+
|
|
35
|
+
# 扫描时跳过的目录
|
|
36
|
+
SKIP_DIRS = {"node_modules", ".next", "generated", ".git", "dist", "build", ".work"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def scan_ts_files(repo_dir: Path, max_files: int = 40) -> list[Path]:
|
|
40
|
+
"""扫 repo 下的 TS/TSX 源文件(跳过依赖/产物目录)。"""
|
|
41
|
+
out: list[Path] = []
|
|
42
|
+
for p in sorted(repo_dir.rglob("*.ts")) + sorted(repo_dir.rglob("*.tsx")):
|
|
43
|
+
if any(part in SKIP_DIRS for part in p.parts):
|
|
44
|
+
continue
|
|
45
|
+
if p.name.endswith(".d.ts") or p.name.endswith(".test.ts"):
|
|
46
|
+
continue
|
|
47
|
+
out.append(p)
|
|
48
|
+
if len(out) >= max_files:
|
|
49
|
+
break
|
|
50
|
+
return out
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _seam_catalog() -> list[dict]:
|
|
54
|
+
core = json.loads((ROOT / "core" / "loom.core.json").read_text(encoding="utf-8"))
|
|
55
|
+
return [{"seam_id": s["seam_id"], "signature": s.get("signature", "")} for s in core["seams"]]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def align_files_to_seams(files: list[Path], repo_dir: Path) -> list[dict]:
|
|
59
|
+
"""AI 判断每个文件对齐到哪个接缝(或 skip)。单轮 JSON 输出。"""
|
|
60
|
+
from openai import OpenAI
|
|
61
|
+
|
|
62
|
+
base = os.environ.get("LOOM_LLM_BASE_URL", "https://api.deepseek.com")
|
|
63
|
+
key = os.environ["LOOM_LLM_API_KEY"]
|
|
64
|
+
model = os.environ.get("LOOM_LLM_MODEL", "deepseek-chat")
|
|
65
|
+
client = OpenAI(api_key=key, base_url=base)
|
|
66
|
+
|
|
67
|
+
seams = _seam_catalog()
|
|
68
|
+
# 给 AI 每个文件的 export 签名摘要(不喂全文,省 input)
|
|
69
|
+
file_digests = []
|
|
70
|
+
for i, f in enumerate(files):
|
|
71
|
+
try:
|
|
72
|
+
exports = extract_exports(f.read_text(encoding="utf-8"))
|
|
73
|
+
except Exception:
|
|
74
|
+
exports = []
|
|
75
|
+
sig = "; ".join(f"{e.kind} {e.name}: {e.signature}" for e in exports[:3]) or "(无 export)"
|
|
76
|
+
file_digests.append(f" [{i}] {f.relative_to(repo_dir)} → {sig[:150]}")
|
|
77
|
+
|
|
78
|
+
sys_prompt = (
|
|
79
|
+
"你是 Loom 的仓库分析器。给定一个 repo 的文件 export 签名清单和 Loom 的接缝(seam)目录,"
|
|
80
|
+
"判断每个文件**对齐到哪个接缝**(该文件实现了那个接缝的能力),或 skip(不对齐任何接缝)。\n"
|
|
81
|
+
"只在签名明确匹配接缝能力时对齐;拿不准就 skip。\n"
|
|
82
|
+
"【接缝目录】\n" + "\n".join(f" {s['seam_id']}: {s['signature']}" for s in seams) + "\n\n"
|
|
83
|
+
'【输出】JSON:{"alignments":[{"file_index":0,"seam_id":"...或null(skip)",'
|
|
84
|
+
'"ref":"建议候选名(kebab-case)","summary":"一句话能力摘要","confidence":0.0-1.0}]}'
|
|
85
|
+
)
|
|
86
|
+
user_msg = "仓库文件清单(index 文件 → export 签名):\n" + "\n".join(file_digests)
|
|
87
|
+
|
|
88
|
+
resp = client.chat.completions.create(
|
|
89
|
+
model=model,
|
|
90
|
+
messages=[{"role": "system", "content": sys_prompt}, {"role": "user", "content": user_msg}],
|
|
91
|
+
response_format={"type": "json_object"},
|
|
92
|
+
max_tokens=4096,
|
|
93
|
+
)
|
|
94
|
+
print(f"[analyze] AI 对齐 input={resp.usage.prompt_tokens} output={resp.usage.completion_tokens} tok")
|
|
95
|
+
return json.loads(resp.choices[0].message.content).get("alignments", [])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# 接缝 → 物化目标目录(与 loom.core.json 的 target 对齐)
|
|
99
|
+
SEAM_TARGET_DIR = {
|
|
100
|
+
"auth.oauth_provider": "src/server/auth/providers/",
|
|
101
|
+
"data.crud_resource": "src/server/api/routers/",
|
|
102
|
+
"ui.data_table": "src/app/_components/",
|
|
103
|
+
"report.custom_export": "src/server/export/",
|
|
104
|
+
"content.markdown_render": "src/app/_components/",
|
|
105
|
+
"data.bulk_import": "src/server/import/",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main() -> None:
|
|
110
|
+
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
|
111
|
+
dry_run = "--dry-run" in sys.argv
|
|
112
|
+
if not args:
|
|
113
|
+
print("用法: analyze_repo.py <repo_dir> [--dry-run]")
|
|
114
|
+
return
|
|
115
|
+
repo_dir = Path(args[0]).resolve()
|
|
116
|
+
if not repo_dir.is_dir():
|
|
117
|
+
print(f"✗ 不是目录: {repo_dir}")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
files = scan_ts_files(repo_dir)
|
|
121
|
+
print(f"=== analyze_repo: {repo_dir.name} ({len(files)} 个 TS 文件) ===")
|
|
122
|
+
if not files:
|
|
123
|
+
print("无可分析的 TS 文件")
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
alignments = align_files_to_seams(files, repo_dir)
|
|
127
|
+
aligned = [a for a in alignments if a.get("seam_id")]
|
|
128
|
+
print(f"\nAI 对齐结果:{len(aligned)}/{len(files)} 文件对齐到接缝\n")
|
|
129
|
+
|
|
130
|
+
ingested = []
|
|
131
|
+
for a in aligned:
|
|
132
|
+
idx = a["file_index"]
|
|
133
|
+
if idx >= len(files):
|
|
134
|
+
continue
|
|
135
|
+
f = files[idx]
|
|
136
|
+
seam = a["seam_id"]
|
|
137
|
+
ref = a.get("ref") or f.stem
|
|
138
|
+
target_dir = SEAM_TARGET_DIR.get(seam)
|
|
139
|
+
if not target_dir:
|
|
140
|
+
print(f" [skip] {f.name}: 未知接缝 {seam}")
|
|
141
|
+
continue
|
|
142
|
+
target = target_dir + f.name
|
|
143
|
+
print(f" [{a.get('confidence',0):.2f}] {f.relative_to(repo_dir)} → {seam} (ref={ref})")
|
|
144
|
+
if dry_run:
|
|
145
|
+
continue
|
|
146
|
+
# 入池:provenance=user(贡献者,高初始信任),过 verify 门见下方提示
|
|
147
|
+
meta_path = ingest_file(
|
|
148
|
+
f, seam, ref, a.get("summary", f"来自 {repo_dir.name} 的贡献"),
|
|
149
|
+
target, CANDIDATES,
|
|
150
|
+
)
|
|
151
|
+
# ingest 默认 provenance=platform,这里改成 user(贡献者)
|
|
152
|
+
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
153
|
+
meta["registry_item"]["meta_loom"]["provenance"] = "user"
|
|
154
|
+
meta["l0"]["provenance"] = "user"
|
|
155
|
+
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
156
|
+
ingested.append(f"{seam}/{ref}")
|
|
157
|
+
|
|
158
|
+
if dry_run:
|
|
159
|
+
print(f"\n[dry-run] 不入池。去掉 --dry-run 实际 ingest。")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
print(f"\n入池 {len(ingested)} 个候选(provenance=user),跑质量门自检…")
|
|
163
|
+
# 飞轮防污染闭环:入池后立即过 verify 门,不过 t3 gate 的自动撤回(前人种坏树自动被拔)
|
|
164
|
+
import subprocess
|
|
165
|
+
|
|
166
|
+
proc = subprocess.run(
|
|
167
|
+
["uv", "run", "python", str(Path(__file__).parent / "verify_candidates.py")],
|
|
168
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
169
|
+
cwd=str(Path(__file__).parent),
|
|
170
|
+
)
|
|
171
|
+
out = (proc.stdout or "") + "\n" + (proc.stderr or "")
|
|
172
|
+
# 解析 verify 输出,找新入池候选里没过门的([✗] seam/ref)
|
|
173
|
+
failed = []
|
|
174
|
+
for line in out.splitlines():
|
|
175
|
+
if "[✗]" in line:
|
|
176
|
+
tag = line.split("[✗]")[1].strip().split()[0] # seam/ref
|
|
177
|
+
if tag in ingested:
|
|
178
|
+
failed.append(tag)
|
|
179
|
+
kept = [c for c in ingested if c not in failed]
|
|
180
|
+
for tag in failed:
|
|
181
|
+
seam, ref = tag.split("/", 1)
|
|
182
|
+
d = CANDIDATES / seam / ref
|
|
183
|
+
if d.exists():
|
|
184
|
+
import shutil
|
|
185
|
+
shutil.rmtree(d)
|
|
186
|
+
print(f"\n✓ 质量门把关完成:")
|
|
187
|
+
print(f" 留池(过 t3 gate):{kept}")
|
|
188
|
+
if failed:
|
|
189
|
+
print(f" 自动撤回(未过门,AI 误判/不合格):{failed}")
|
|
190
|
+
print(f" → 飞轮防污染:贡献被 AI 拆解入池,质量门自动拦截毒树。")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
main()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""一次性:给候选 meta.json 的 meta_loom 回填 tradeoffs(架构取舍)字段。
|
|
2
|
+
|
|
3
|
+
第二步「候选级梯度呈现」配套:目标用户是架构师,挑候选时要看依赖/复杂度/适用场景。
|
|
4
|
+
只在 registry_item.meta_loom 里加 tradeoffs,不动任何其他字段。幂等。
|
|
5
|
+
|
|
6
|
+
用法:cd platform && uv run python backfill_tradeoffs.py [--dry-run]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
16
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
17
|
+
|
|
18
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
19
|
+
CAND = ROOT / "candidates"
|
|
20
|
+
|
|
21
|
+
# 候选 ref → tradeoffs(基于实际源码理解写,不瞎编能力)
|
|
22
|
+
TRADEOFFS: dict[str, str] = {
|
|
23
|
+
# ── ui.data_table ──
|
|
24
|
+
"simple-data-table": "零外部依赖,原生 table。功能基础(无排序/分页/筛选)。适合快速展示小数据量,不适合大数据或需交互的场景。",
|
|
25
|
+
"tanstack-data-table": "依赖 @tanstack/react-table(需装包),支持排序等高级能力。适合复杂交互表格,代价是引入运行时依赖与学习成本。",
|
|
26
|
+
"paginated-data-table": "零依赖,客户端分页(可选 pageSize)。适合中等数据量分页浏览;纯客户端分页,超大数据集仍需服务端分页。",
|
|
27
|
+
"sortable-data-table": "零依赖,点击表头排序(localeCompare 数字感知)。适合需排序但不想引 tanstack 的场景;排序在客户端,大数据量有性能上限。",
|
|
28
|
+
"filterable-data-table": "零依赖,顶部全局文本筛选(跨列子串匹配)。适合带搜索的列表;仅文本包含匹配,无高级筛选条件。",
|
|
29
|
+
"compact-data-table": "零依赖,紧凑只读(小行距、无阴影圆角)。适合 dashboard 卡片内密集展示;纯展示无交互。",
|
|
30
|
+
"selectable-data-table": "零依赖,多选行(复选框列 + 全选 + 已选计数)。适合批量操作场景;选择状态在组件内,需自行接批量动作。",
|
|
31
|
+
"striped-numeric-data-table": "零依赖,斑马纹 + 数字列自动右对齐。适合财务/报表只读展示;按值类型判断对齐,纯展示无交互。",
|
|
32
|
+
# ── report.custom_export ──
|
|
33
|
+
"csv-export-fn": "零依赖,浏览器原生 CSV(处理逗号/引号/换行转义)。通用导出首选;Excel 打开中文可能乱码(用 excel-csv 版)。",
|
|
34
|
+
"excel-csv-export-fn": "零依赖,带 UTF-8 BOM 的 CSV,解决 Excel 中文乱码。面向 Excel 用户首选;纯文本/程序消费场景用普通 csv 即可。",
|
|
35
|
+
"json-export-fn": "零依赖,pretty JSON + 列投影。适合程序间数据交换/调试;不适合给非技术用户。",
|
|
36
|
+
"tsv-export-fn": "零依赖,制表符分隔。Excel/Sheets 粘贴友好、少引号转义问题;字段内含制表符会被替换为空格。",
|
|
37
|
+
"clipboard-export-fn": "零依赖,复制 CSV 到剪贴板(navigator.clipboard)。适合快速复制粘贴;依赖浏览器剪贴板权限,需 https/用户手势。",
|
|
38
|
+
"markdown-export-fn": "零依赖,GFM 表格。适合贴进 README/issue/文档;不适合数据量大或需机器解析的场景。",
|
|
39
|
+
# ── auth.oauth_provider ──
|
|
40
|
+
"google-oauth": "NextAuth Google provider,接 authConfig.providers[]。覆盖最广的消费级登录;需配 Google OAuth 凭据。",
|
|
41
|
+
"github-oauth": "NextAuth GitHub provider。面向开发者/技术产品的登录首选;非技术用户覆盖不如 Google。",
|
|
42
|
+
"credentials-auth": "NextAuth Credentials(账号密码)。无第三方依赖、自主可控;需自行管理密码安全/哈希,安全责任更重。",
|
|
43
|
+
# ── data.crud_resource ──
|
|
44
|
+
"project-crud-router": "tRPC router,专为 Project 资源的完整 CRUD(list/get/create/update/delete)。需对应 prisma model;最贴合标准 CRUD 需求。",
|
|
45
|
+
"generic-crud-factory": "泛型 CRUD 工厂,按资源名生成 router。适合多资源复用、减少样板;抽象层带来理解成本,定制单点逻辑较绕。",
|
|
46
|
+
"readonly-list": "只读列表 router(仅 list/get)。适合纯展示/报表类资源;不支持写操作,需要增删改时不适用。",
|
|
47
|
+
"post-router": "tRPC router,Post 资源 CRUD(t3 经典示例)。适合博客/内容类;字段固定为 Post 语义,其他资源需改写。",
|
|
48
|
+
# ── 其他 ──
|
|
49
|
+
"markdown-view": "Markdown 渲染组件。适合内容展示;具体依赖见 deps,注意 XSS(渲染不可信内容需 sanitize)。",
|
|
50
|
+
"table-markdown-view": "支持 GFM 表格的 markdown 渲染。适合带表格的文档/笔记;inline 渲染走 dangerouslySetInnerHTML,不可信内容需 sanitize。",
|
|
51
|
+
"code-block-markdown-view": "支持 ``` 代码块的 markdown 渲染,代码原样转义。适合技术博客/文档;无语法高亮(要高亮需引 highlight.js 等)。",
|
|
52
|
+
"minimal-markdown-view": "极简 markdown,只渲染标题+段落、不解析 inline。最安全最轻;功能也最少,适合可信度低或只需基础排版。",
|
|
53
|
+
"csv-contacts-import": "CSV 批量导入,字段固定 name/description(Contact 语义)。适合通讯录类;其他实体需改字段映射。",
|
|
54
|
+
"generic-csv-import": "通用 CSV 解析,首行表头→Record<string,string>,不绑定实体。适合任意表格导入;调用方自行映射到 model。",
|
|
55
|
+
"json-import": "JSON 数组批量导入,值转字符串。适合从 API/导出的 JSON 导入;非数组/非法 JSON 返回空。",
|
|
56
|
+
"tsv-import": "TSV 制表符分隔导入。适合 Excel/Sheets 复制粘贴;字段含制表符会错列。",
|
|
57
|
+
# ── file.upload(新 seam)──
|
|
58
|
+
"data-url-upload": "base64 data URL 上传,零后端零依赖。适合 MVP/头像等小文件预览;不持久化、大文件会撑爆内存。",
|
|
59
|
+
"presigned-url-upload": "PUT 到预签名 URL(S3/R2/OSS)。适合生产对象存储;需后端先签发 URL,本函数只管上传动作。",
|
|
60
|
+
# ── ui.form ──
|
|
61
|
+
"simple-form": "基础受控表单,纯 text 输入。最简单,适合快速录入;无校验、无字段类型,复杂表单不够用。",
|
|
62
|
+
"validated-form": "带必填校验的表单,空必填字段阻止提交。适合需基础校验的创建/编辑;仅必填校验,无格式/异步校验。",
|
|
63
|
+
"typed-form": "多字段类型(text/textarea/select)。适合字段多样的实体编辑;类型有限,无日期/文件等高级控件。",
|
|
64
|
+
# ── ui.layout ──
|
|
65
|
+
"sidebar-layout": "侧边栏布局,左导航+右内容。适合后台管理(多入口);窄屏需自行处理折叠。",
|
|
66
|
+
"topbar-layout": "顶栏布局,水平导航+居中内容。适合内容站/营销页/少入口应用;入口多会挤。",
|
|
67
|
+
# ── ui.detail ──
|
|
68
|
+
"field-list-detail": "字段列表式(dl/dt/dd)详情。紧凑、适合多字段;纯展示无操作按钮。",
|
|
69
|
+
"card-detail": "卡片式详情,带标题网格布局。视觉更突出、适合单条资源;字段极多时网格会很长。",
|
|
70
|
+
# ── auth 扩展 ──
|
|
71
|
+
"magic-link": "邮箱+验证码登录(Credentials 实现,零依赖)。适合无第三方 OAuth 的自主登录;验证码校验是骨架,生产需接邮件服务+存储。",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main() -> None:
|
|
76
|
+
dry = "--dry-run" in sys.argv
|
|
77
|
+
done = 0
|
|
78
|
+
missing: list[str] = []
|
|
79
|
+
for meta_path in sorted(CAND.rglob("meta.json")):
|
|
80
|
+
data = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
81
|
+
ml = data.get("registry_item", {}).get("meta_loom")
|
|
82
|
+
if ml is None:
|
|
83
|
+
continue
|
|
84
|
+
ref = data.get("l0", {}).get("ref") or meta_path.parent.name
|
|
85
|
+
tr = TRADEOFFS.get(ref)
|
|
86
|
+
if tr is None:
|
|
87
|
+
missing.append(ref)
|
|
88
|
+
continue
|
|
89
|
+
ml["tradeoffs"] = tr
|
|
90
|
+
if not dry:
|
|
91
|
+
meta_path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
92
|
+
print(f" {'[dry] ' if dry else '✓ '}{ref}")
|
|
93
|
+
done += 1
|
|
94
|
+
|
|
95
|
+
print(f"\n回填 {done} 个候选{'(dry-run)' if dry else ''}。")
|
|
96
|
+
if missing:
|
|
97
|
+
print(f"⚠ 无 tradeoffs 文案的 ref(需补 TRADEOFFS 表):{missing}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
main()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""实测:用 loom.alhz.org 远程 server 做一个项目,省多少 token。
|
|
2
|
+
|
|
3
|
+
测的是宿主 AI 的 OUTPUT token(最贵的部分):
|
|
4
|
+
- 用 Loom:AI 看 propose 候选 → 输出"选哪个"(极小);代码由 get_files 返回,AI 不生成
|
|
5
|
+
- 不用 Loom:AI 从零写每个组件文件(auth/crud/table…),全是 AI output
|
|
6
|
+
|
|
7
|
+
用真 tiktoken(cl100k_base) 计 token,经公网真实调用。
|
|
8
|
+
"""
|
|
9
|
+
import httpx, json, tiktoken
|
|
10
|
+
|
|
11
|
+
URL = "https://loom.alhz.org/mcp"
|
|
12
|
+
H = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"}
|
|
13
|
+
enc = tiktoken.get_encoding("cl100k_base")
|
|
14
|
+
tok = lambda s: len(enc.encode(s))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse(t):
|
|
18
|
+
for line in t.splitlines():
|
|
19
|
+
if line.startswith("data: "):
|
|
20
|
+
return json.loads(line[6:])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
with httpx.Client(timeout=40) as c:
|
|
24
|
+
r = c.post(URL, headers=H, json={"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
|
25
|
+
"params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "bench", "version": "1"}}})
|
|
26
|
+
H["mcp-session-id"] = r.headers.get("mcp-session-id")
|
|
27
|
+
c.post(URL, headers=H, json={"jsonrpc": "2.0", "method": "notifications/initialized"})
|
|
28
|
+
|
|
29
|
+
# 1. propose:server 返回候选梯度(这是 AI 的 INPUT,读它来选)
|
|
30
|
+
idea = open("../ideas/saas-admin-with-google-auth.json", encoding="utf-8").read()
|
|
31
|
+
r = c.post(URL, headers=H, json={"jsonrpc": "2.0", "id": 2, "method": "tools/call",
|
|
32
|
+
"params": {"name": "loom_propose", "arguments": {"idea_json": idea}}})
|
|
33
|
+
prop = parse(r.text)["result"]["content"][0]["text"]
|
|
34
|
+
|
|
35
|
+
# 2. get_files:拿到选中候选的真实代码(= AI 免于生成的代码量)
|
|
36
|
+
plan = json.dumps({"idea_id": "saas", "core_ref": "create-t3-app@7.39.x", "seams": [
|
|
37
|
+
{"seam_id": "auth.oauth_provider", "action": "pick", "ref": "google-oauth", "confidence": 1, "why": "x"},
|
|
38
|
+
{"seam_id": "data.crud_resource", "action": "pick", "ref": "project-crud-router", "confidence": 1, "why": "x"},
|
|
39
|
+
{"seam_id": "ui.data_table", "action": "pick", "ref": "simple-data-table", "confidence": 1, "why": "x"},
|
|
40
|
+
], "synthesized": [], "budget": {"input_tok": 0, "output_tok": 0}})
|
|
41
|
+
r = c.post(URL, headers=H, json={"jsonrpc": "2.0", "id": 3, "method": "tools/call",
|
|
42
|
+
"params": {"name": "loom_get_files", "arguments": {"plan_json": plan}}})
|
|
43
|
+
gf = json.loads(parse(r.text)["result"]["content"][0]["text"])
|
|
44
|
+
|
|
45
|
+
# 选中候选的代码(AI 不用自己写的)
|
|
46
|
+
cand_keys = ["providers/google.ts", "routers/project.ts", "_components/data-table.tsx"]
|
|
47
|
+
cand_files = [f for f in gf["files"] if any(k in f["path"] for k in cand_keys)]
|
|
48
|
+
cand_code = "\n".join(f["content"] for f in cand_files)
|
|
49
|
+
|
|
50
|
+
# ── 用 Loom:AI 的 output = 选择决策(3 个 seam,每个就是 "选 X 因为 Y")
|
|
51
|
+
loom_output = '选择:auth→google-oauth(精确匹配Google登录);crud→project-crud-router(专为Project的完整CRUD);table→simple-data-table(零依赖)。'
|
|
52
|
+
loom_out_tok = tok(loom_output)
|
|
53
|
+
|
|
54
|
+
# ── 不用 Loom:AI 要从零写这些组件代码(output = 这些代码本身)
|
|
55
|
+
fromzero_out_tok = tok(cand_code)
|
|
56
|
+
|
|
57
|
+
print("=== 实测:经 loom.alhz.org 做一个 SaaS 后台(auth+CRUD+表格 3 组件)===")
|
|
58
|
+
print(f"propose 候选梯度大小(AI 读的 input): {tok(prop)} tok")
|
|
59
|
+
print(f"get_files 返回代码: {sum(tok(f['content']) for f in cand_files)} tok({len(cand_files)} 个候选文件)")
|
|
60
|
+
print()
|
|
61
|
+
print(f"【用 Loom】AI output(只输出选择): {loom_out_tok} tok")
|
|
62
|
+
print(f"【不用 Loom】AI output(从零写这 3 个组件): {fromzero_out_tok} tok")
|
|
63
|
+
saved = (1 - loom_out_tok / fromzero_out_tok) * 100
|
|
64
|
+
print(f"→ AI output 省: {saved:.0f}%({fromzero_out_tok} → {loom_out_tok})")
|
|
65
|
+
print()
|
|
66
|
+
print("注:只算 3 个 pick 组件。真实项目组件更多,省得更多;但从零写也可能更省字(AI偷工),故此为量级参考。")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""读 .work/bench-archive 的多样本双臂 metrics,汇总成诚实的实测报告。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import json, statistics, sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
8
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
9
|
+
|
|
10
|
+
ARCHIVE = Path(__file__).resolve().parent.parent / ".work" / "bench-archive"
|
|
11
|
+
|
|
12
|
+
# data[idea][arm] = list of (output_tok, converged)
|
|
13
|
+
data: dict[str, dict[str, list]] = defaultdict(lambda: defaultdict(list))
|
|
14
|
+
for f in sorted(ARCHIVE.glob("*.json")):
|
|
15
|
+
# 文件名:<arm>-<idea>-run<N>.json,arm 可能含下划线(from_zero)
|
|
16
|
+
name = f.stem
|
|
17
|
+
arm = "from_zero" if name.startswith("from_zero") else "assembly"
|
|
18
|
+
rest = name[len(arm) + 1:] # 去掉 arm- 前缀
|
|
19
|
+
idea = rest.rsplit("-run", 1)[0]
|
|
20
|
+
d = json.loads(f.read_text(encoding="utf-8"))
|
|
21
|
+
data[idea][arm].append((d["total_output_tok"], d["converged"]))
|
|
22
|
+
|
|
23
|
+
print("=== Loom 双臂对照实测(多样本)===\n")
|
|
24
|
+
print(f"{'想法':<34} {'臂':<10} {'样本':>4} {'out中位':>8} {'out范围':>14} {'收敛率':>8}")
|
|
25
|
+
print("-" * 84)
|
|
26
|
+
|
|
27
|
+
all_asm_out, all_fz_out = [], []
|
|
28
|
+
for idea in sorted(data):
|
|
29
|
+
for arm in ("assembly", "from_zero"):
|
|
30
|
+
runs = data[idea].get(arm, [])
|
|
31
|
+
if not runs:
|
|
32
|
+
continue
|
|
33
|
+
outs = [r[0] for r in runs]
|
|
34
|
+
convs = [r[1] for r in runs]
|
|
35
|
+
med = int(statistics.median(outs))
|
|
36
|
+
conv_rate = f"{sum(convs)}/{len(convs)}"
|
|
37
|
+
print(f"{idea:<34} {arm:<10} {len(runs):>4} {med:>8} {f'{min(outs)}-{max(outs)}':>14} {conv_rate:>8}")
|
|
38
|
+
(all_asm_out if arm == "assembly" else all_fz_out).extend(outs)
|
|
39
|
+
|
|
40
|
+
print("-" * 84)
|
|
41
|
+
if all_asm_out and all_fz_out:
|
|
42
|
+
am, fm = statistics.median(all_asm_out), statistics.median(all_fz_out)
|
|
43
|
+
print(f"\n全样本 output 中位数:assembly={am} from_zero={fm}")
|
|
44
|
+
print(f"量级比:from_zero / assembly ≈ {fm/am:.1f}×(组装省 AI 输出 ≈ {(1-am/fm)*100:.0f}%)")
|
|
45
|
+
print(f"样本量:assembly {len(all_asm_out)} 次,from_zero {len(all_fz_out)} 次")
|