abelien-harness 0.6.0 → 0.6.1

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.
package/README.md CHANGED
@@ -243,7 +243,7 @@ skill-lint 双向守卫核对声明与实际 import 一致,打包校验再确
243
243
 
244
244
  | 层 | 篇目 | 一句话 |
245
245
  | --- | --- | --- |
246
- | architecture | [state-machine](docs/architecture/state-machine.md) / [workflow-persistence](docs/architecture/workflow-persistence.md) / [httl](docs/architecture/httl.md) / [host-adapters](docs/architecture/host-adapters.md) / [generate3d](docs/architecture/generate3d.md) / [structure](docs/architecture/structure.md) / [structure-worker](docs/architecture/structure-worker.md) / [question-card](docs/architecture/question-card.md) / [visualization-rsi](docs/architecture/visualization-rsi.md) | 双层 FSM · 定义即数据/实例即文件 · HTTL 协议 · claude-code/codex 双入口收敛 · 声明式 provider + 异步任务引擎 · 外观/结构协作边界 · 问题卡与 ACL 前端协议(contract-ready) · `ab --visual` 可视化与自进化收口 |
246
+ | architecture | [state-machine](docs/architecture/state-machine.md) / [workflow-persistence](docs/architecture/workflow-persistence.md) / [httl](docs/architecture/httl.md) / [host-adapters](docs/architecture/host-adapters.md) / [generate3d](docs/architecture/generate3d.md) / [structure](docs/architecture/structure.md) / [structure-worker](docs/architecture/structure-worker.md) / [structure-worker-protocol](docs/architecture/structure-worker-protocol.md) / [structure-cad-library](docs/architecture/structure-cad-library.md) / [structure-component-fit](docs/architecture/structure-component-fit.md) / [structure-worker-implementation](docs/architecture/structure-worker-implementation.md) / [question-card](docs/architecture/question-card.md) / [visualization-rsi](docs/architecture/visualization-rsi.md) | 双层 FSM · 定义即数据/实例即文件 · HTTL 协议 · claude-code/codex 双入口收敛 · 声明式 provider + 异步任务引擎 · 外观/结构协作边界 · 问题卡与 ACL 前端协议(contract-ready) · `ab --visual` 可视化与自进化收口 |
247
247
  | paradigms | [README](docs/paradigms/README.md) + development / testing / evaluation / self-optimization | 场景工件是唯一事实源:测试测零件、评估测整车、优化答归因 |
248
248
  | reference | [product-journey](docs/reference/product-journey.md) / [research-notes](docs/reference/research-notes.md) / [verification-method](docs/reference/verification-method.md) | PRD 旅程转译 · 基准研究与 EaaS 方法论 · 验证方法→场景判定器(每篇带 来源/取舍) |
249
249
  | mvp | [journey](docs/mvp/journey.md) / [coverage](docs/mvp/coverage.md) | 历史 MVP(已移除的 25 步及真实实录)· 四维覆盖矩阵 + 缺口账本 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abelien-harness",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "npx-installable multi-agent skill harness: generic kernel + pluggable domain packs",
5
5
  "type": "module",
6
6
  "engines": {
@@ -29,7 +29,15 @@ compatibility:
29
29
  - 比例判定由测量值和规格计算得出,不能由 Agent 自填。
30
30
 
31
31
  所选候选比例不符时,结果会写明冲突,不会被隐藏。
32
- 5. **交回**:final 结果的 `items` 就是画布网格的格子,`unknowns` 是需要如实展示的缺口。宿主展示的是证据,不是作品已确认。
32
+ 5. **交回**:final 结果的 `items` 就是画布网格的格子,`unknowns` 是需要如实展示的缺口,`budget` 说明检索有没有因上限停下。宿主展示的是证据,不是作品已确认。
33
+
34
+ ## 用户换选与排除(K6)
35
+
36
+ 首版之前不向用户提问;用户可以随时在画布或对话里改变参考。处理方式都是对**同一份收集结果**重新提交判断,不重新检索:
37
+
38
+ - **换成某个候选**(画布「换成这款/就是这款」,或原话点名「参考产品换成 X」):沿用原 `evidence_id`,把 `identity.chosen_item_id` 改成用户点名的候选,重新调用 `reference.finalize`。比例冲突照常由核验算出并写进 `identity.conflict`,如实展示,不再追问。
39
+ - **不要参考某一条**(「不要参考这个」):重新提交判断,把该条目放进 `excluded`,`reason` 写明是用户不采用。被排除的是所选产品时,改选其余候选中比例一致的一个;没有合适的就把 `chosen_item_id` 设为 `null`,并在 `unknowns` 写明型号待定。
40
+ - 这两种情况都属于需求修订:clarify 把用户的选择记成 source=user 的需求,后续阶段按 workspace 规则推进,本包只负责重新提交判断。
33
41
 
34
42
  ## 调用
35
43
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abelien-reference",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "参考研究域包:按 Agent 的检索计划收集产品识别、逆向资料与相似产品证据(检索、公开页面、主图、用户原图测量),再核验 Agent 的判断,产出 reference-evidence v1;判定码来自确定性检查",
5
5
  "requires": [
6
6
  "abelien-harness"
@@ -2,7 +2,9 @@
2
2
  "budget": {
3
3
  "images": 0,
4
4
  "pages": 1,
5
- "queries": 1
5
+ "queries": 1,
6
+ "skipped_pages": 0,
7
+ "skipped_images": 0
6
8
  },
7
9
  "changeable": [
8
10
  "外形",
@@ -184,6 +184,8 @@
184
184
  "budget": {
185
185
  "queries": 1,
186
186
  "pages": 2,
187
- "images": 1
187
+ "images": 1,
188
+ "skipped_pages": 0,
189
+ "skipped_images": 0
188
190
  }
189
191
  }
@@ -2,7 +2,9 @@
2
2
  "budget": {
3
3
  "images": 1,
4
4
  "pages": 2,
5
- "queries": 1
5
+ "queries": 1,
6
+ "skipped_pages": 0,
7
+ "skipped_images": 0
6
8
  },
7
9
  "changeable": [],
8
10
  "depth": "assembly",
@@ -31,6 +31,10 @@
31
31
 
32
32
  输出 `reference-evidence`,status=collected。`items` 中第一项是 `user_input`,其后每个页面对应一格:识别用途为 `candidate`,逆向用途为 `teardown`,相似用途为 `similar`。读不到的页面状态为 `unavailable`。此时还没有任何 `verdict`。
33
33
 
34
+ `budget` 记录本次用量和截断:`queries`、`pages`、`images` 是已检索条数、已读页面数、已抓主图数;`skipped_pages` 是检索到、但因 `max_pages` 没有读取的去重公开页面数,`skipped_images` 是有主图、但因 `max_images` 没有抓取的页面数。宿主据此说明「已达本阶段检索上限」,不能把没读到当作没有。final 结果原样带上收集时的 `budget`。
35
+
36
+ 请求摘要包含证据格式版本(`EVIDENCE_SHAPE`,当前为 2)。旧版本 harness 收集的证据保留原 id,不会被复用,也不会被覆盖;同一请求按新格式重新收集,得到新 id。
37
+
34
38
  ## reference.finalize
35
39
 
36
40
  ### 输入
@@ -4,7 +4,7 @@
4
4
  "title": "abelien.reference-evidence.v1",
5
5
  "description": "参考研究证据。status=collected 由 reference.research 产出,只有收集结果;status=final 由 reference.finalize 产出,带核验过的识别、相似产品与判定。宿主只展示与转存,不自行改写判定。",
6
6
  "type": "object",
7
- "required": ["id", "schema", "status", "intent", "keep", "changeable", "depth", "text", "user_image", "searches", "pages", "images", "items", "unknowns"],
7
+ "required": ["id", "schema", "status", "intent", "keep", "changeable", "depth", "text", "user_image", "searches", "pages", "images", "items", "unknowns", "budget"],
8
8
  "properties": {
9
9
  "id": { "type": "string", "pattern": "^ref-[0-9a-f]{16}(-f[0-9a-f]{8})?$" },
10
10
  "schema": { "const": "abelien.reference-evidence.v1" },
@@ -35,6 +35,19 @@
35
35
  "images": { "type": "array", "items": { "$ref": "#/$defs/image" } },
36
36
  "items": { "type": "array", "items": { "$ref": "#/$defs/item" } },
37
37
  "unknowns": { "type": "array", "items": { "type": "string" } },
38
+ "budget": {
39
+ "description": "本次研究的用量:已检索条数、已读页面数、已抓主图数,以及因计划上限没有读取的去重公开页面数与主图数;宿主据此说明「已达检索上限」,不把没读到当作没有",
40
+ "type": "object",
41
+ "required": ["queries", "pages", "images", "skipped_pages", "skipped_images"],
42
+ "properties": {
43
+ "queries": { "type": "integer", "minimum": 0 },
44
+ "pages": { "type": "integer", "minimum": 0 },
45
+ "images": { "type": "integer", "minimum": 0 },
46
+ "skipped_pages": { "type": "integer", "minimum": 0 },
47
+ "skipped_images": { "type": "integer", "minimum": 0 }
48
+ },
49
+ "additionalProperties": false
50
+ },
38
51
  "collected_id": { "type": "string", "description": "status=final 时为来源收集结果的 id" },
39
52
  "judgement_digest": { "type": "string" },
40
53
  "identity": {
@@ -15,6 +15,11 @@ export const VERDICT_CODES = Object.freeze(["closest", "ratio_mismatch", "exclud
15
15
  export const EVIDENCE_STATUSES = Object.freeze(["collected", "final"]);
16
16
  export const KIND_BY_PURPOSE = Object.freeze({ identify: "candidate", teardown: "teardown", similar: "similar" });
17
17
 
18
+ /**
19
+ * Version of the collected shape, part of every request digest: evidence collected by an older harness (e.g. without
20
+ * budget.skipped_*) keeps its own id and is never reused or overwritten; the same request collects afresh under a new id.
21
+ */
22
+ export const EVIDENCE_SHAPE = 2;
18
23
  export const LIMITS = Object.freeze({ queries: 6, count: 5, pages: 12, images: 8, links: 5, textChars: 4000, queryChars: 200,
19
24
  pageBytes: 8 * 1024 * 1024, imageBytes: 4 * 1024 * 1024, userImageBytes: 16 * 1024 * 1024, excerptChars: 1200, pageTimeoutMs: 30000, searchTimeoutMs: 30000, concurrency: 4 });
20
25
 
@@ -49,6 +54,9 @@ export function validateEvidence(body) {
49
54
  if (!INTENTS.includes(body.intent) || (body.depth !== null && !DEPTHS.includes(body.depth))) invalid("intent or depth is invalid");
50
55
  stringList(body.keep, "keep"); stringList(body.changeable, "changeable"); stringList(body.unknowns, "unknowns", 200);
51
56
  for (const name of ["searches", "pages", "images", "items"]) if (!Array.isArray(body[name])) invalid(`${name} must be an array`);
57
+ const budget = body.budget, counts = ["queries", "pages", "images", "skipped_pages", "skipped_images"];
58
+ if (!record(budget) || Object.keys(budget).length !== counts.length
59
+ || counts.some(key => !Number.isSafeInteger(budget[key]) || budget[key] < 0)) invalid(`budget must count ${counts.join(", ")}`);
52
60
  body.items.forEach(checkItem);
53
61
  const ids = body.items.map(item => item.id); if (new Set(ids).size !== ids.length) invalid("item ids must be unique");
54
62
  if (body.status === "final") {
@@ -6,7 +6,7 @@ import { text, record, digest, sha256 } from "../../abelien-harness/scripts/web/
6
6
  import { decodeBody } from "../../abelien-harness/scripts/web/public-read.mjs";
7
7
  import { isPng } from "./png.mjs";
8
8
  import { measurePng } from "./measure.mjs";
9
- import { SCHEMA, INTENTS, DEPTHS, PURPOSES, KIND_BY_PURPOSE, LIMITS, reader, imageReader, volc, invalid, stringList, site, validateEvidence } from "./evidence.mjs";
9
+ import { SCHEMA, INTENTS, DEPTHS, PURPOSES, KIND_BY_PURPOSE, LIMITS, EVIDENCE_SHAPE, reader, imageReader, volc, invalid, stringList, site, validateEvidence } from "./evidence.mjs";
10
10
 
11
11
  function normalizeLink(value) {
12
12
  try { return reader.normalizePublicSourceUrl(value).href; } catch { invalid("links must be public HTTPS URLs without credentials"); }
@@ -70,12 +70,20 @@ async function pool(list, task, limit = LIMITS.concurrency) {
70
70
  return out;
71
71
  }
72
72
 
73
- /** Interleave search results so one query cannot use the whole page budget. */
73
+ /**
74
+ * Interleave search results so one query cannot use the whole page budget. Also counts the distinct public pages the
75
+ * cap left unread, so the host can say the research stopped at its limit instead of implying nothing else was found.
76
+ */
74
77
  function pageQueue(links, intent, searches, max) {
75
78
  const queue = [], seen = new Set();
76
- const push = entry => { if (queue.length < max && !seen.has(entry.url)) { seen.add(entry.url); queue.push(entry); } };
79
+ let skipped = 0;
80
+ const push = entry => {
81
+ if (seen.has(entry.url)) return;
82
+ seen.add(entry.url);
83
+ if (queue.length < max) queue.push(entry); else skipped++;
84
+ };
77
85
  for (const url of links) push({ url, purpose: intent === "create" ? "similar" : "identify", search_id: null, query: null, search_title: null });
78
- for (let rank = 0; queue.length < max; rank++) {
86
+ for (let rank = 0; ; rank++) {
79
87
  let added = false;
80
88
  for (const search of searches) {
81
89
  const result = search.results[rank];
@@ -85,16 +93,19 @@ function pageQueue(links, intent, searches, max) {
85
93
  }
86
94
  if (!added) break;
87
95
  }
88
- return queue;
96
+ return Object.assign(queue, { skipped });
89
97
  }
90
98
 
91
99
  export async function researchReference({ input, env = {}, fetchImpl, readResource, writeBlob, ports = {} }) {
92
100
  const request = validateResearchInput(input);
93
101
  if (typeof writeBlob !== "function" || typeof readResource !== "function") throw Object.assign(new Error("trusted content writer and resource reader are required"), { code: "REFERENCE_HOST" });
94
- const requestDigest = digest({ ...request, userImage: request.userImage?.sha256 ?? null });
102
+ const requestDigest = digest({ ...request, userImage: request.userImage?.sha256 ?? null, shape: EVIDENCE_SHAPE });
95
103
  const id = `ref-${requestDigest.slice(0, 16)}`;
96
104
  const existing = await readResource(`reference-evidence:${id}`);
97
- if (existing?.body?.status === "collected" && existing.body.request_digest === requestDigest) return existing.body;
105
+ if (existing?.body?.status === "collected" && existing.body.request_digest === requestDigest) {
106
+ // Never reuse a body that fails today's contract; the kernel then refuses to overwrite it, so the conflict surfaces.
107
+ try { return validateEvidence(existing.body); } catch { /* fall through: the checkpoint reports the conflict */ }
108
+ }
98
109
 
99
110
  const unknowns = [], items = [];
100
111
  let userImage = null;
@@ -145,7 +156,7 @@ export async function researchReference({ input, env = {}, fetchImpl, readResour
145
156
  });
146
157
  for (const page of pages) if (page.status !== "read") unknowns.push(`页面不可用:${page.url}(${page.reason})`);
147
158
  // Phase 2: main images for the first pages that have one, up to the image budget, fetched concurrently.
148
- const wanted = pages.filter(page => page.image_url).slice(0, request.plan.max_images);
159
+ const withImage = pages.filter(page => page.image_url), wanted = withImage.slice(0, request.plan.max_images);
149
160
  const images = await pool(wanted, async (page, index) => {
150
161
  const entryImage = { id: `i${index + 1}`, url: page.image_url, page_id: page.id, status: "unavailable", reason: null, blob_ref: null, sha256: null, media_type: null, measurement: null };
151
162
  try {
@@ -174,6 +185,7 @@ export async function researchReference({ input, env = {}, fetchImpl, readResour
174
185
  return validateEvidence({
175
186
  id, schema: SCHEMA, status: "collected", request_digest: requestDigest, intent: request.intent, keep: request.keep, changeable: request.changeable,
176
187
  depth: request.depth, text: request.text, user_image: userImage, searches, pages, images, items, unknowns,
177
- budget: { queries: searches.length, pages: pages.length, images: images.length },
188
+ budget: { queries: searches.length, pages: pages.length, images: images.length,
189
+ skipped_pages: queue.skipped, skipped_images: withImage.length - wanted.length },
178
190
  });
179
191
  }