diff --git a/data/state/corpus_usage_mobile.json b/data/state/corpus_usage_mobile.json index e0ae5f8..e4148f0 100644 --- a/data/state/corpus_usage_mobile.json +++ b/data/state/corpus_usage_mobile.json @@ -1,6 +1,6 @@ { "version": 1, - "last_updated": "2026-05-15T07:01:20.311Z", + "last_updated": "2026-05-15T07:22:50.645Z", "usage": { "① 个人生活类": { "Travel Memory Scrapbook": 2, diff --git a/docs/index.html b/docs/index.html index 8e6916e..c5652f7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -885,9 +885,11 @@
topics[i % length] rotation makes consecutive batches pick identical
- corpus topics. We persist data/state/corpus_usage.json — usage counts per
- (l2_key, topic) — and prefer least-used topics for each new run. 100% → 0% topic
- overlap with previous batch.
+ corpus topics. We persist per-platform state — corpus_usage_mobile.json /
+ corpus_usage_web.json — usage counts per (l2_key, topic) — and prefer
+ least-used topics for each new run. 100% → 0% topic overlap with previous batch.
+ When a pool is fully exhausted, [Layer-A WARN] fires and
+ grow-corpus.js can expand it.
queries.jsonl + plan.jsonl pair. Subsequent runs auto-update on success;
- failed rows are not counted toward usage. Inspect data/state/corpus_usage.json any time.
+ failed rows are not counted toward usage. State is per-platform:
+ data/state/corpus_usage_mobile.json / corpus_usage_web.json.
+ When a pool is exhausted, run node scripts/grow-corpus.js --platform <p> to expand it.
corpus_usage.json state · 5-bucket opener hash (Build a / Need a / Create a / Make a / no opener) keyed by query_id · persona-tone semantic mapping from L2 → 5 ordinary-user archetypes.corpus_usage_{platform}.json state · 5-bucket opener hash (Build a / Need a / Create a / Make a / no opener) keyed by query_id · persona-tone semantic mapping from L2 → 5 ordinary-user archetypes.corpus_usage.json state · 5-bucket opener hash (Build a / Need a / Create a / Make a / no opener) keyed by query_id · persona-tone semantic mapping from L2 → 5 ordinary-user archetypes.",
+ "ev2.s2.f":"Layer-A least-used topic dedup across batches via per-platform corpus_usage_{platform}.json state · 5-bucket opener hash (Build a / Need a / Create a / Make a / no opener) keyed by query_id · persona-tone semantic mapping from L2 → 5 ordinary-user archetypes.",
"ev2.s2.r":"Cross-batch corpus-topic overlap: 100% → 0%. \"Build a\" share: 54% → 21%. 5 distinct persona voices visible in batch.",
"ev2.s2.p":"Audit found 49.5% of queries still framed as \"Build a XX page where…\" — the opener is now diverse but the scope noun is still page-level, causing downstream LLMs to generate single-page mocks instead of complete apps.",
"ev2.s3.t":"App-scope rewrite",
@@ -1415,7 +1419,7 @@ topics[i % length] rotation makes consecutive batches pick identical corpus topics. We persist data/state/corpus_usage.json — usage counts per (l2_key, topic) — and prefer least-used topics for each new run. 100% → 0% topic overlap with previous batch.",
+ "div.f3.d":"Naive topics[i % length] rotation makes consecutive batches pick identical corpus topics. We persist per-platform state — corpus_usage_mobile.json / corpus_usage_web.json — usage counts per (l2_key, topic) — and prefer least-used topics for each new run. 100% → 0% topic overlap with previous batch. When a pool is fully exhausted, [Layer-A WARN] fires and grow-corpus.js can expand it.",
"div.f3.r1":"topic overlap with prior batch",
"div.f4.t":"Opener hash distribution",
"div.f4.d":"With \"mobile H5\" in the system prompt, 54% of v3 outputs converged on Build a mobile .... We deterministically hash query_id into one of 5 opener buckets (Build a / Need a / Create a / Make a / no formal opener), forcing uniform distribution. Idempotent across reruns.",
@@ -1428,7 +1432,7 @@ queries.jsonl + plan.jsonl pair. Subsequent runs auto-update on success; failed rows are not counted toward usage. Inspect data/state/corpus_usage.json any time.",
+ "div.bs.d":"first time enabling Layer-A, seed the state file from a historical queries.jsonl + plan.jsonl pair. Subsequent runs auto-update on success; failed rows are not counted toward usage. State is per-platform: data/state/corpus_usage_mobile.json / corpus_usage_web.json. When a pool is exhausted, run node scripts/grow-corpus.js --platform <p> to expand it.",
"div.tab.bars":"// L1 distribution",
"div.tab.network":"// Corpus network",
"div.cv.note":"Each ⬤ inner node is an L1 category; · outer dot is an L2 sub-scene. Hover to highlight a branch · click any node to drill in.",
@@ -1483,7 +1487,7 @@ corpus_usage.json)· 5 桶 opener 哈希(Build a / Need a / Create a / Make a / 无 opener)按 query_id 决定性分配 · persona-tone 按 L2 语义最佳匹配映射到 5 个普通用户 archetype。",
+ "ev2.s2.f":"Layer-A 跨批次最少使用 topic 优先(按平台持久化到 corpus_usage_{platform}.json)· 5 桶 opener 哈希(Build a / Need a / Create a / Make a / 无 opener)按 query_id 决定性分配 · persona-tone 按 L2 语义最佳匹配映射到 5 个普通用户 archetype。",
"ev2.s2.r":"跨批次 corpus_topic 重叠率:100% → 0%。「Build a」开头占比:54% → 21%。批次内 5 种 persona 口吻清晰可辨。",
"ev2.s2.p":"审计发现 49.5% 的 query 仍然以「Build a XX page where…」框定 —— opener 已分散,但 scope 名词仍在 page 级,导致下游 LLM 生成单页 mock 而非完整 app。",
"ev2.s3.t":"App-scope 改写",
@@ -1537,7 +1541,7 @@ topics[i % length] 轮换会让相邻批次反复挑到同样的 corpus topic。我们把每个 (l2_key, topic) 的累计使用次数持久化到 data/state/corpus_usage.json,新批次优先选最少使用过的 topic。100% → 0% 跨批次 topic 重叠。",
+ "div.f3.d":"朴素的 topics[i % length] 轮换会让相邻批次反复挑到同样的 corpus topic。我们按平台分别持久化 usage state(corpus_usage_mobile.json / corpus_usage_web.json),记录每个 (l2_key, topic) 的累计使用次数,新批次优先选最少使用过的 topic。100% → 0% 跨批次 topic 重叠。topic 池耗尽时触发 [Layer-A WARN],可用 grow-corpus.js 扩池。",
"div.f3.r1":"与上一批 topic 重叠率",
"div.f4.t":"开头哈希均匀分配",
"div.f4.d":"system prompt 里有 \"mobile H5\",v3 输出 54% 都收敛到 Build a mobile ...。我们用 query_id 决定性哈希到 5 个开头桶之一(Build a / Need a / Create a / Make a / 无 formal opener),强制均匀分布;同 query_id 跑多次拿到同一开头。",
@@ -1550,7 +1554,7 @@ queries.jsonl + plan.jsonl 一次性导入历史 usage。后续 run 跑成功的条目自动累加;失败的不计入。任何时候打开 data/state/corpus_usage.json 即可查看当前状态。",
+ "div.bs.d":"首次启用 Layer-A 时,可从已有 queries.jsonl + plan.jsonl 一次性导入历史 usage。后续 run 跑成功的条目自动累加;失败的不计入。State 按平台隔离:data/state/corpus_usage_mobile.json / corpus_usage_web.json。topic 池耗尽后,运行 node scripts/grow-corpus.js --platform <p> 扩池。",
"div.tab.bars":"// L1 分布",
"div.tab.network":"// 语料网络",
"div.cv.note":"⬤ 内圈节点 是 L1 类目;· 外圈点 是 L2 子场景。悬停高亮分支 · 点击任意节点下钻查看。",
diff --git a/mvp/query_factory_v2.js b/mvp/query_factory_v2.js
index 6711d46..99105de 100644
--- a/mvp/query_factory_v2.js
+++ b/mvp/query_factory_v2.js
@@ -2611,7 +2611,7 @@ function allocateCorpusCountsByScenePlan(spec, total, options = {}) {
* @param {object} usageMap - { topic: usageCount }; missing key counts as 0
* @returns {string[]} - picked topics, length === count
*/
-function pickLeastUsedTopics(topics, count, usageMap = {}) {
+function pickLeastUsedTopics(topics, count, usageMap = {}, label = "") {
if (!topics.length) return [];
// Annotate with original index for stable tie-break
const annotated = topics.map((topic, idx) => ({
@@ -2621,6 +2621,15 @@ function pickLeastUsedTopics(topics, count, usageMap = {}) {
}));
// Sort: least-used first, then original index
annotated.sort((a, b) => (a.use - b.use) || (a.idx - b.idx));
+ // Warn when pool is exhausted and cycling will start
+ if (count > topics.length) {
+ const minUse = annotated[0].use;
+ const tag = label ? ` [${label}]` : "";
+ console.warn(
+ `[Layer-A WARN]${tag} topic pool exhausted: requested ${count} but pool has only ${topics.length}` +
+ ` (min usage=${minUse}). Topics will cycle — consider running grow-corpus to expand the pool.`
+ );
+ }
// Cycle if count > topics.length
const picked = [];
for (let i = 0; i < count; i += 1) {
@@ -2723,7 +2732,14 @@ function buildCorpusPlan(spec, corpusData, options = {}) {
// Layer-A: pick topics by least-used-first across batches.
// Tracks intra-batch increments locally so duplicate picks within this run are also avoided.
const localUsage = { ...(corpusUsage[l2Key] || {}) };
- const pickedTopics = pickLeastUsedTopics(topics, count, localUsage);
+ const usedCount = Object.keys(localUsage).length;
+ if (topics.length > 0 && usedCount >= topics.length) {
+ console.warn(
+ `[Layer-A WARN] [${l2Key}] all ${topics.length} topics already used at least once` +
+ ` (used=${usedCount}, need=${count}). Next batch will reuse topics.`
+ );
+ }
+ const pickedTopics = pickLeastUsedTopics(topics, count, localUsage, l2Key);
for (let i = 0; i < count; i += 1) {
const groupIndex = Math.floor(i / Math.max(MN, 1));
diff --git a/scripts/README.md b/scripts/README.md
index cf8d65d..4b03bcb 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -81,56 +81,52 @@ npm run run:free -- --persona-scope task --no-resume
**工作流(与 `run:free` 并存的独立入口):**
```
-parseRequirementsFromWorkbook(xlsx) # 解析 61 个 L2
+parseRequirementsFromWorkbook(xlsx) # 解析场景(mobile: xlsx;web: JSON spec)
↓
-buildCorpusPlan(spec, corpus, { # 按 xlsx L1 配比分配 N 个 task
- total: 200, # 总量(必填默认)
- complexityMix: ["medium"], # 复杂度轮换(默认全 medium)
+buildCorpusPlan(spec, corpus, { # 按场景配比分配 N 个 task;Layer-A 最少使用采样
+ total: 500,
+ complexityMix: ["medium"],
+ corpusUsage: loadCorpusUsage(...) # data/state/corpus_usage_{platform}.json
})
↓
-buildCorpusDirectQueryPrompt(task) # 显式锁 corpus_topic,禁招呼语
+buildCorpusDirectQueryPrompt(task) # 显式锁 corpus_topic;Layer-B opener hash;Layer-C persona 注入
↓
claude CLI subprocess(lib/claude-cli.js) # 走 packy CC 网关,model=claude-sonnet-4-6
- ↓
-scoreQueryRecord # 启发式打分
+ ↓ # —— 或 --prep-only 模式:跳过此步,写 _subagent_in/ ——
+scoreQueryRecord # 启发式打分(--score 开启)
↓
data/output/corpus_run/
- ├── plan.jsonl # 完整计划(每 task 含 corpus_topic)
- ├── queries.jsonl # 每条 query + score + word_count + duration
- └── summary.json # 汇总:L1 分布 / 平均质量 / 通过率 / 耗时
+ ├── plan.jsonl # 完整计划(每 task 含 corpus_topic + corpus_l2_key)
+ ├── queries.jsonl # 每条 query + word_count + duration(+ score 如启用)
+ ├── queries.xlsx # 同内容 Excel(默认生成)
+ ├── summary.json # 汇总:L1 分布 / 平均质量 / 通过率 / 耗时 / platform
+ └── _subagent_in/ # --prep-only 时:subagent 批次输入文件
+ └── {platform}_b01_in.json …
```
**用法:**
```bash
-# 默认:200 task,全 medium,按 xlsx L1 配比分布
+# 默认:200 task,全 medium,mobile 平台
node scripts/run-corpus.js
-# 自定义总量
-node scripts/run-corpus.js --total 500
+# 生产批次:500 条 web 端
+node scripts/run-corpus.js --platform web --total 500 --out data/output/corpus_run_web_500
# 自定义复杂度 mix
node scripts/run-corpus.js --total 200 --complexity-mix "vague,medium,medium"
# 验证 plan 分布(不调 LLM)
-node scripts/run-corpus.js --total 200 --dry-run
-
-# 验证用:只跑前 N 条真实 LLM
-node scripts/run-corpus.js --total 200 --limit 5
+node scripts/run-corpus.js --total 500 --dry-run
# 提高并发
-node scripts/run-corpus.js --total 200 --concurrency 4
-
-# 自定义输出
-node scripts/run-corpus.js --total 200 --out data/output/corpus_v1
+node scripts/run-corpus.js --total 500 --concurrency 8
# 排除 L1 场景(子串匹配,逗号分隔)
node scripts/run-corpus.js --total 200 --exclude-l1 "深度研究,购物消费"
-# 自定义 Layer-A state / persona-map 路径
-node scripts/run-corpus.js --total 200 \
- --usage-state data/state/run_alpha.json \
- --persona-map scripts/corpus_persona_map.json
+# No-API 模式:只生成 subagent 批次文件,不调 LLM(详见下方「No-API 模式」章节)
+node scripts/run-corpus.js --platform mobile --total 500 --prep-only --out data/output/corpus_run_mobile_500
# 关掉 usage 跟踪(一次性试跑、不污染历史)
node scripts/run-corpus.js --total 200 --no-usage-track
@@ -140,26 +136,149 @@ node scripts/run-corpus.js --total 200 --no-usage-track
| 参数 | 默认 | 说明 |
|---|---|---|
-| `--total` | `200` | 总 task 数;按 xlsx L1 配比缩放分配 |
+| `--platform` | `mobile` | 平台:`mobile`(xlsx 场景 + `corpus_data.json`)或 `web`(JSON spec + `corpus_data_web.json`) |
+| `--total` | `200` | 总 task 数;按场景配比缩放分配 |
| `--complexity-mix` | `"medium"` | 复杂度轮换(逗号分隔),如 `"vague,medium,medium"` |
| `--concurrency` | `2` | claude CLI 子进程并发数 |
| `--dry-run` | 关 | 不调 LLM,验证 plan 分布与脚本结构 |
-| `--limit N` | 关 | 仅执行前 N 个 task |
-| `--input` | 自动 | xlsx 路径(默认从 `data/input/` 自动检测) |
+| `--limit N` | 关 | 仅执行前 N 个 task(调试用) |
+| `--input` | 自动 | xlsx 路径(mobile 平台;默认从 `data/input/` 自动检测) |
| `--out` | `data/output/corpus_run` | 输出目录 |
-| `--exclude-l1` | 无 | L1 场景子串过滤(逗号分隔),如 `"深度研究,购物消费"` |
-| `--usage-state` | `data/state/corpus_usage.json` | Layer-A 跨批次 topic 去重 state 文件 |
+| `--exclude-l1` | 无 | L1 场景子串过滤(逗号分隔) |
+| `--usage-state` | `data/state/corpus_usage_{platform}.json` | Layer-A 跨批次 topic 去重 state;不同平台自动隔离 |
| `--no-usage-track` | 关 | 关闭 Layer-A 跟踪(一次性试跑、不污染历史)|
-| `--persona-map` | `scripts/corpus_persona_map.json` | Layer-C L2 → persona 语义映射文件 |
+| `--persona-map` | `scripts/corpus_persona_map[_web].json` | Layer-C L2 → persona 语义映射文件 |
+| `--prep-only` | 关 | No-API 模式:只写 subagent 批次文件,不调 LLM(见下方章节) |
+| `--prep-batch` | `25` | `--prep-only` 每批次 task 数 |
+| `--score` | 关 | 开启启发式质量评分 |
+| `--no-xlsx` | 关 | 跳过 xlsx 导出 |
**三层多样性机制(默认全部启用):**
-- **Layer-A 跨批次去重**:`data/state/corpus_usage.json` 记录 `(l2_key, topic)` 累计使用次数;新批次优先选 least-used,与历史 batch topic 重叠 100% → 0%
+- **Layer-A 跨批次去重**:`data/state/corpus_usage_{platform}.json` 记录 `(l2_key, topic)` 累计使用次数;新批次优先选 least-used,与历史 batch topic 重叠 100% → 0%。当某 L2 topic 池全部耗尽时输出 `[Layer-A WARN]` 提示(此时可运行 `grow-corpus.js` 扩池)
- **Layer-B Opener hash**:`query_id` 决定性哈希到 5 桶之一(`Build a` / `Need a` / `Create a` / `Make a` / 无 formal opener),破除模型在 "Build a..." 上的收敛
-- **Layer-C Persona-tone 语义映射**:`scripts/corpus_persona_map.json` 按 L2 语义匹配 5 种普通用户 persona(`maker` / `planner` / `curator` / `operator` / `founder_like`),prompt 注入 voice 描述 + dev jargon 黑名单,把含 dev 术语的 query 占比从 20%(v4)压到 0.5%(v5)
+- **Layer-C Persona-tone 语义映射**:`scripts/corpus_persona_map[_web].json` 按 L2 语义匹配 5 种普通用户 persona(`maker` / `planner` / `curator` / `operator` / `founder_like`),prompt 注入 voice 描述 + dev jargon 黑名单,把含 dev 术语的 query 占比从 20%(v4)压到 0.5%(v5)
**复用关系:** 共享 `scripts/lib/claude-cli.js` 与 `test-corpus-methods.js`,两脚本对 claude CLI 调用统一一处实现。
+## No-API 模式:`--prep-only` + subagent + merge
+
+> 当 Packy API 额度耗尽或需要零 token 成本生产时,使用此三步流程。
+
+**流程:**
+
+```
+Step 1 run-corpus.js --prep-only
+ → 生成 plan.jsonl + 占位 queries.jsonl(全 error=PREP_ONLY_PENDING)
+ → 写