abelien-harness 0.3.0 → 0.4.0

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 (45) hide show
  1. package/README.md +9 -3
  2. package/package.json +1 -1
  3. package/skills/abelien-3dgen/SKILL.md +4 -2
  4. package/skills/abelien-3dgen/providers/tripo.json +64 -1
  5. package/skills/abelien-3dgen/references/engineering-lowpoly-qualification.md +20 -0
  6. package/skills/abelien-3dgen/references/host-api.md +128 -0
  7. package/skills/abelien-3dgen/references/provider-profile.md +4 -0
  8. package/skills/abelien-appearance/SKILL.md +2 -0
  9. package/skills/abelien-appearance/references/local-component-edit.md +31 -0
  10. package/skills/abelien-clarify/SKILL.md +8 -3
  11. package/skills/abelien-clarify/references/examples.md +9 -0
  12. package/skills/abelien-clarify/scripts/check_result.py +53 -1
  13. package/skills/abelien-clarify/scripts/test_check_result.py +40 -0
  14. package/skills/abelien-guide/SKILL.md +2 -0
  15. package/skills/abelien-guide/references/shared-response-rules.md +2 -2
  16. package/skills/abelien-hardware/SKILL.md +5 -0
  17. package/skills/abelien-hardware/domain.json +2 -2
  18. package/skills/abelien-hardware/references/search-index-format.md +31 -2
  19. package/skills/abelien-hardware/references/search-index-release.md +9 -0
  20. package/skills/abelien-hardware/references/selection-service-contract.md +3 -0
  21. package/skills/abelien-hardware/scripts/build-search-index.mjs +50 -6
  22. package/skills/abelien-hardware/scripts/dimensions.mjs +184 -0
  23. package/skills/abelien-hardware/scripts/finalize-selection.mjs +11 -0
  24. package/skills/abelien-hardware/scripts/publish-search-index.mjs +2 -1
  25. package/skills/abelien-hardware/scripts/search-index-release.mjs +15 -2
  26. package/skills/abelien-hardware/scripts/tos-wiki-search.mjs +38 -20
  27. package/skills/abelien-hardware-selection/domain.json +2 -2
  28. package/skills/abelien-hardware-selection/references/selection-service-contract.md +12 -1
  29. package/skills/abelien-hardware-selection/scripts/select.mjs +9 -0
  30. package/skills/abelien-harness/references/workflow-spec.md +15 -2
  31. package/skills/abelien-harness/scripts/generate3d-host.mjs +398 -0
  32. package/skills/abelien-harness/scripts/generate3d.mjs +54 -15
  33. package/skills/abelien-harness/scripts/receipt.mjs +8 -2
  34. package/skills/abelien-harness/scripts/studio.mjs +18 -10
  35. package/skills/abelien-structure/SKILL.md +3 -1
  36. package/skills/abelien-structure/references/build123d-execution.md +1 -1
  37. package/skills/abelien-structure/scripts/run_stage.py +8 -1
  38. package/skills/abelien-workspace/SKILL.md +2 -0
  39. package/skills/abelien-workspace/domain.json +1 -1
  40. package/skills/abelien-workspace/workflows/definitions/a6e8254e1839d94530ae243cba779e423953b7169cbce338f0503d748d44800b.json +175 -0
  41. package/skills/abelien-workspace/workflows/definitions/b37f3b47e1e8bd1208859de57456152b149416e21949297b22bba937c51a5670.json +149 -0
  42. package/skills/abelien-workspace/workflows/definitions/b780bf280d61596b43eb6dbc6a417cf8df1ec9a7ae79a6de10f048da9ed58729.json +229 -0
  43. package/skills/abelien-workspace/workflows/user-journey-hardware-algorithm.json +1 -1
  44. package/skills/abelien-workspace/workflows/user-journey-hardware-candidates.json +1 -1
  45. package/skills/abelien-workspace/workflows/user-journey-hardware.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import zlib from "node:zlib";
2
+ import { dimensionFields, evaluateDimensions, orderByDimensionTier, sameDimensionMetadata, screenDimensions, shortlistByDimensionTier, validateRequirementEnvelopes } from "./dimensions.mjs";
2
3
  import { selectionRequestDigest, validCategoryPath } from "./selection-request.mjs";
3
4
  import { createTosConfig, getTosObject, sha256 } from "./tos-object-store.mjs";
4
5
 
@@ -81,11 +82,17 @@ export function parseHardwareWiki(content) {
81
82
  "schema", "type", "title", "description", "manufacturer", "model", "aliases", "resource_urn", "record_id",
82
83
  "record_version", "status", "category_path", "category_leaf", "primary_function", "integration_level",
83
84
  "hardware_form", "component_kind", "functional_capabilities", "capability_coverage", "source_url", "source_updated_at",
85
+ "dimensions_mm", "dimension_basis", "dimension_geometry", "dimension_source", "interfaces",
84
86
  ];
85
87
  const meta = {};
86
88
  for (const key of wanted) {
87
89
  const line = frontMatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
88
- if (line) meta[key] = scalar(line[1]);
90
+ if (line) {
91
+ if (key === "dimension_source" || key === "dimensions_mm") {
92
+ try { meta[key] = JSON.parse(line[1]); }
93
+ catch { throw error("WIKI_INVALID", `${key} must be generated inline JSON`); }
94
+ } else meta[key] = scalar(line[1]);
95
+ }
89
96
  }
90
97
  meta.selection_profile = {
91
98
  suitable_for: nestedInline(frontMatter, "selection_profile", "suitable_for") ?? [],
@@ -199,6 +206,8 @@ function parseCategoryIndex(object, descriptor, key) {
199
206
  }
200
207
  if (recordIds.has(record.record_id)) throw error("WIKI_SEARCH_INDEX_INVALID", "category index contains duplicate record_id", { key, record_id: record.record_id });
201
208
  recordIds.add(record.record_id);
209
+ if (!/^[a-f0-9]{64}$/.test(record.sha256 ?? "")) throw error("WIKI_SEARCH_INDEX_INVALID", "category index requires every Wiki content SHA-256; rebuild legacy indexes", { key, record_id: record.record_id });
210
+ dimensionFields(record);
202
211
  record.path = safeRelativeObjectKey(record.path, "Wiki path");
203
212
  }
204
213
  return index;
@@ -218,6 +227,7 @@ async function mapConcurrent(items, concurrency, mapper) {
218
227
  }
219
228
 
220
229
  function validateInput(input) {
230
+ validateRequirementEnvelopes(input?.requirements);
221
231
  if (!input || typeof input !== "object" || Array.isArray(input)) throw error("VALIDATION", "hardware search input must be an object");
222
232
  if (!Array.isArray(input.slots) || input.slots.length === 0) throw error("VALIDATION", "hardware search requires non-empty slots");
223
233
  for (const slot of input.slots) {
@@ -258,6 +268,9 @@ function candidateFromDocument(document, config, ranking, includeContent, indexS
258
268
  manufacturer: meta.manufacturer == null ? "Unspecified" : String(meta.manufacturer),
259
269
  model: meta.model == null ? meta.model : String(meta.model),
260
270
  title: meta.title,
271
+ name: meta.title,
272
+ ...dimensionFields(meta),
273
+ ...(meta.interfaces !== undefined ? { interfaces: meta.interfaces } : {}),
261
274
  category_path: meta.category_path,
262
275
  integration_level: meta.integration_level,
263
276
  hardware_form: meta.hardware_form,
@@ -279,7 +292,7 @@ function candidateFromDocument(document, config, ranking, includeContent, indexS
279
292
  };
280
293
  }
281
294
 
282
- function createWikiLoader(config, fetchImpl) {
295
+ function createWikiLoader(config, fetchImpl, { legacy = false } = {}) {
283
296
  const cache = new Map();
284
297
  return entry => {
285
298
  if (!cache.has(entry.path)) {
@@ -290,9 +303,10 @@ function createWikiLoader(config, fetchImpl) {
290
303
  path: entry.path, indexed_record_id: entry.record_id, wiki_record_id: parsed.meta.record_id,
291
304
  });
292
305
  }
293
- if (entry.sha256 && sha256(parsed.content) !== entry.sha256) {
306
+ if (!/^[a-f0-9]{64}$/.test(entry.sha256 ?? "") || sha256(parsed.content) !== entry.sha256) {
294
307
  throw error("WIKI_SEARCH_INDEX_MISMATCH", "Wiki content SHA-256 does not match its search index", { path: entry.path });
295
308
  }
309
+ if (!legacy && !sameDimensionMetadata(parsed.meta, entry)) throw error("WIKI_SEARCH_INDEX_MISMATCH", "Wiki dimension metadata does not match its search index", { path: entry.path });
296
310
  return { ...parsed, etag: object.etag, lastModified: object.lastModified, entry };
297
311
  }));
298
312
  }
@@ -300,7 +314,7 @@ function createWikiLoader(config, fetchImpl) {
300
314
  };
301
315
  }
302
316
 
303
- async function searchWithCategoryIndexes({ input, env, config, catalogObject, fetchImpl, topK, includeContent }) {
317
+ async function searchWithCategoryIndexes({ input, env, config, catalogObject, fetchImpl, topK, maxFetch, includeContent }) {
304
318
  const catalogKey = `${config.prefix}_catalog.json`;
305
319
  const { categories } = parseCatalog(catalogObject, catalogKey);
306
320
  const indexCache = new Map();
@@ -314,7 +328,7 @@ async function searchWithCategoryIndexes({ input, env, config, catalogObject, fe
314
328
  if (!indexCache.has(descriptor.index_key)) {
315
329
  indexCache.set(descriptor.index_key, getObject(config, `${config.prefix}${descriptor.index_key}`, fetchImpl).then(object => {
316
330
  const index = parseCategoryIndex(object, descriptor, `${config.prefix}${descriptor.index_key}`);
317
- shardSnapshots.push(`${descriptor.index_key}:${object.etag}`);
331
+ shardSnapshots.push(`${descriptor.index_key}:${object.etag}:${object.raw_sha256}`);
318
332
  return index;
319
333
  }));
320
334
  }
@@ -333,23 +347,26 @@ async function searchWithCategoryIndexes({ input, env, config, catalogObject, fe
333
347
  }
334
348
  const index = await loadIndex(descriptor);
335
349
  const semanticQuery = semanticQueryFor(input, slot);
336
- const rankedIndex = index.records.map(entry => ({
337
- entry,
350
+ const screen = screenDimensions(index.records, input.requirements);
351
+ const rankedIndex = screen.survivors.map(({ record: entry, check }) => ({
352
+ entry, check,
338
353
  ranking: scoreIndexEntry(entry, semanticQuery, input.policy?.prefer_system_candidates !== false),
339
354
  })).sort((a, b) => b.ranking.score - a.ranking.score || String(a.entry.model).localeCompare(String(b.entry.model)));
340
- const shortlist = rankedIndex.slice(0, topK);
355
+ const shortlist = shortlistByDimensionTier(rankedIndex, topK, maxFetch, item => item.check.status);
341
356
  const loaded = await mapConcurrent(shortlist, concurrency, async item => ({
342
357
  document: await wikiLoader(item.entry), indexRanking: item.ranking,
343
358
  }));
344
- const candidates = loaded.map(({ document, indexRanking }) => {
359
+ const candidates = orderByDimensionTier(loaded.map(({ document, indexRanking }) => {
345
360
  const ranking = scoreDocument(document, semanticQuery, input.policy?.prefer_system_candidates !== false);
346
- return candidateFromDocument(document, config, ranking, includeContent, indexRanking.score);
347
- }).sort((a, b) => b.semantic_score - a.semantic_score || b.index_score - a.index_score || String(a.model).localeCompare(String(b.model)));
361
+ const candidate = candidateFromDocument(document, config, ranking, includeContent, indexRanking.score);
362
+ return { ...candidate, dimension_check: evaluateDimensions(candidate, input.requirements) };
363
+ }).sort((a, b) => b.semantic_score - a.semantic_score || b.index_score - a.index_score || String(a.model).localeCompare(String(b.model))), candidate => candidate.dimension_check.status);
348
364
  if (candidates.length === 0) unmatchedSlots.push(slot.slot_id);
349
365
  results.push({
350
366
  slot_id: slot.slot_id,
351
367
  category_path: slot.category_path,
352
368
  retrieval_mode: "category_index",
369
+ dimension_screen: screen.summary,
353
370
  available_documents: index.records.length,
354
371
  indexed_candidates: index.records.length,
355
372
  fetched_documents: shortlist.length,
@@ -361,19 +378,19 @@ async function searchWithCategoryIndexes({ input, env, config, catalogObject, fe
361
378
  results,
362
379
  unmatchedSlots,
363
380
  unknowns: [],
364
- sourceSnapshot: `tos:${config.bucket}:${config.prefix}:catalog:${catalogObject.etag}:indexes:${shardsDigest}`,
381
+ sourceSnapshot: `tos:${config.bucket}:${config.prefix}:catalog:${catalogObject.etag}:indexes:${shardsDigest}:content:${catalogObject.raw_sha256}`,
365
382
  retrievalMode: "category_index",
366
383
  };
367
384
  }
368
385
 
369
- async function searchWithLegacyManifest({ input, env, config, fetchImpl, topK, includeContent }) {
386
+ async function searchWithLegacyManifest({ input, env, config, fetchImpl, topK, maxFetch, includeContent }) {
370
387
  const manifestKey = `${config.prefix}_manifest.json`;
371
388
  const manifestObject = await getObject(config, manifestKey, fetchImpl);
372
389
  const manifest = parseJsonObject(manifestObject, "WIKI_MANIFEST_INVALID", "TOS hardware Wiki manifest is not valid JSON", manifestKey);
373
390
  if (!Array.isArray(manifest)) throw error("WIKI_MANIFEST_INVALID", "TOS hardware Wiki manifest must be an array", { key: manifestKey });
374
391
  const scanLimit = Number.isInteger(input.policy?.scan_limit_per_slot) && input.policy.scan_limit_per_slot > 0
375
392
  ? input.policy.scan_limit_per_slot : null;
376
- const wikiLoader = createWikiLoader(config, fetchImpl);
393
+ const wikiLoader = createWikiLoader(config, fetchImpl, { legacy: true });
377
394
  const results = [];
378
395
  const unmatchedSlots = [];
379
396
  const unknowns = [{ code: "SEARCH_INDEX_FALLBACK", message: "category search catalog missing; legacy Manifest full scan was explicitly enabled" }];
@@ -385,10 +402,11 @@ async function searchWithLegacyManifest({ input, env, config, fetchImpl, topK, i
385
402
  }
386
403
  const loaded = await mapConcurrent(entries, envPositiveInteger(env, "TOS_SEARCH_CONCURRENCY", 8), wikiLoader);
387
404
  const semanticQuery = semanticQueryFor(input, slot);
388
- const candidates = loaded.map(document => {
405
+ const candidates = shortlistByDimensionTier(loaded.map(document => {
389
406
  const ranking = scoreDocument(document, semanticQuery, input.policy?.prefer_system_candidates !== false);
390
- return candidateFromDocument(document, config, ranking, includeContent, ranking.score);
391
- }).sort((a, b) => b.semantic_score - a.semantic_score || String(a.model).localeCompare(String(b.model))).slice(0, topK);
407
+ const candidate = candidateFromDocument(document, config, ranking, includeContent, ranking.score);
408
+ return { ...candidate, dimension_check: evaluateDimensions(candidate, input.requirements) };
409
+ }).filter(candidate => candidate.dimension_check.status !== "incompatible").sort((a, b) => b.semantic_score - a.semantic_score || String(a.model).localeCompare(String(b.model))), topK, maxFetch, candidate => candidate.dimension_check.status);
392
410
  if (candidates.length === 0) unmatchedSlots.push(slot.slot_id);
393
411
  results.push({
394
412
  slot_id: slot.slot_id, category_path: slot.category_path, retrieval_mode: "legacy_manifest_full_scan",
@@ -423,9 +441,9 @@ export async function searchHardware({ input, env, fetchImpl = fetch }) {
423
441
  });
424
442
  }
425
443
  const searched = catalogObject
426
- ? await searchWithCategoryIndexes({ input, env, config, catalogObject, fetchImpl, topK, includeContent })
427
- : await searchWithLegacyManifest({ input, env, config, fetchImpl, topK, includeContent });
428
- const id = `search-${sha256(JSON.stringify({ query: input.query ?? "", slots: input.slots, sourceSnapshot: searched.sourceSnapshot })).slice(0, 16)}`;
444
+ ? await searchWithCategoryIndexes({ input, env, config, catalogObject, fetchImpl, topK, maxFetch: maxFullWikiFetch, includeContent })
445
+ : await searchWithLegacyManifest({ input, env, config, fetchImpl, topK, maxFetch: maxFullWikiFetch, includeContent });
446
+ const id = `search-${sha256(JSON.stringify({ query: input.query ?? "", request_digest: selectionRequestDigest(input), slots: input.slots, sourceSnapshot: searched.sourceSnapshot })).slice(0, 16)}`;
429
447
  return {
430
448
  id,
431
449
  status: "completed",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "abelien-hardware-selection",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "本地硬件选型:真实 TOS 召回、Agent 证据判断与受信资源校验;不是数据库兼容性求解器",
5
5
  "resources": ["hardware-selection"],
6
6
  "requires": ["abelien-harness", "abelien-hardware"],
7
7
  "capabilities": {
8
8
  "hardware.select": {
9
- "description": "回读同需求的真实召回资源,验证 Agent 选择的型号、数量、逐约束引用与兼容评估,保存十字段选型;未知不通过门禁",
9
+ "description": "回读同需求的真实召回资源,验证 Agent 选择的型号、数量、逐约束引用与兼容评估,保存选型及可选尺寸占用摘要;未知不通过门禁",
10
10
  "adapter": {
11
11
  "type": "module",
12
12
  "path": "scripts/select.mjs",
@@ -28,7 +28,7 @@ constraint_checks 非数组、ID 重复或不属于当前槽位时仍以 VALIDAT
28
28
 
29
29
  ## 输出与门禁
30
30
 
31
- 保留 Web 既有十字段:id/status/source_snapshot/revision/plan_mode/selected_models/coverage_ledger/remaining_slots/unknowns/replacement_plan。ID/版本由输入和可信召回摘要确定。数量账本由代码计算,模型原始 coverage 和 compatibility 评估仍保留。型号标记 verification_scope=wiki_evidence_review_not_engineering_test、device_tested=false。
31
+ 无结构化 envelope 时保留 Web 既有十字段:id/status/source_snapshot/revision/plan_mode/selected_models/coverage_ledger/remaining_slots/unknowns/replacement_plan。ID/版本由输入和可信召回摘要确定。数量账本由代码计算,模型原始 coverage 和 compatibility 评估仍保留。型号标记 verification_scope=wiki_evidence_review_not_engineering_test、device_tested=false。
32
32
 
33
33
  verified 只代表同源、完整数量和有归属证据的 Agent 选型判断通过,不代表 CAD、几何、电气实测、采用或交付。缺关键评估、非空 unknowns、未覆盖槽位均返回 needs_confirmation;机器门拒绝,但保留结果。整机继承/覆盖 manifest 求解尚未实现,adapt_system 始终待确认;不能借 assemble_new 自动抵扣其他槽位。换型仍走 hardware.revise,不重新排序未点名组件。
34
34
 
@@ -55,3 +55,14 @@ V2 hardware evidence uses fixed prismer_then_volc ordered primary/fallback recei
55
55
  ## 审计 v1 终态与受信 blob 读取
56
56
 
57
57
  新运行开启 `AB_HARNESS_AUDIT_V1=true` 后,needs_confirmation 进入 selected_pending_confirmation,并以组装的 selectionId/selectionStatus/selectionRevision 结束 workflow。此状态仍待用户确认,不能当作 verified 或已采用;硬件/算法组合旅程跳过全部算法步骤。默认运行保留既有守卫。新引用证据的确定性重建需要 readResource 与受信 readBlob,可选 loopback HTTP wrapper 同样传入该端口;缺失时拒绝,不重新抓取来源。安装前无定义摘要和新运行标记的硬件运行保持只读,迁移边界见内核 workflow 规范。
58
+
59
+
60
+ ## 结构化尺寸与正式门禁
61
+
62
+ limits 可附 envelope;严格形状与来源规则见 [索引尺寸契约](../../abelien-hardware/references/search-index-format.md)。hardware.search 在分类所有索引记录上先筛尺寸再 Top-K,未知和估算保留。hardware.select 读取冻结 recall 并核对 request_digest、正文 SHA-256 后,复用纯判定器重算,忽略模型伪造的尺寸、来源、名称和 dimension_check。incompatible 以 VALIDATION 拒绝所选件,必须选其他存活候选;dimension_check.decision_required=true(用户 hard 上限+measured/datasheet 尺寸,且轴对齐装法全部失败,仍不足以证明放不下)时加入 DIMENSION_DECISION_REQUIRED 并返回 needs_confirmation,模型引用、确认或机械 verified 不能覆盖。unknown、估算尺寸、system/preference 上限或估算壁厚下的 risk 只随 selected_models.dimension_check 提示,不进入 unknowns,也不单独阻止 verified;尺寸限制仍须经原 constraint_checks 的 Wiki 证据门。
63
+
64
+ 没有 envelope 时旧流程仍可在原 Wiki 证据范围 verified,但 selected_models.dimension_check 始终显式为 unknown(PRODUCT_ENVELOPE_MISSING),不能宣传尺寸检查通过。clear 也只表示单件粗筛;device_tested 仍 false。hardware.finalize 仍只返回 proposed/needs_confirmation 的提案,不创建硬件正式资源。
65
+
66
+ 每个型号额外携带可信候选 title 派生的 name、dimensions_mm、dimension_basis、dimension_geometry、dimension_source 和 dimension_check;interfaces 只在候选存在时透传。bom_item_id 为 `selection-line-<24hex>`,以 slot_id/component_id 派生的稳定选型行身份,不冒称数据库 BOM 主键,不使用模型提供的 ID。
67
+
68
+ 有 envelope 时输出增加可选 occupancy:advisory=true、method、estimated_volume_mm3、unknown_lines、containers(limit_id/cavity_volume_mm3/ratio)。只统计 selected_models 的实际数量,按 bom_item_id 去重,不再次加入 coverage_ledger 的整机内部覆盖。它是包围体积估算,非装箱证明;unknown 基础的占位尺寸不计入体积,仅保留 unknown_lines;全未知时不输出容器密度。部分未知时体积/比例只含已知或显式估算行,不代表整体密度。浮点溢出/下溢等不可表示的几何量保持 unknown,无法表示的体积或比例省略,不序列化 NaN/Infinity;没有隐含硬阈值。工具函数只在调用方显式提供 occupancy_warning_ratio 策略时给 warning,默认没有告警阈值。接入方须接受这一可选加法,但不得将 occupancy 或 ratio 作为通过/拒绝权威。
@@ -1,4 +1,6 @@
1
1
  // Shared local/HTTP contract. Quotes verify attribution, not engineering test results.
2
+ import { evaluateDimensions, validateRequirementEnvelopes } from "../../abelien-hardware/scripts/dimensions.mjs";
3
+ import { createHash } from "node:crypto";
2
4
  import { mergeHardwareWeb } from "../../abelien-hardware/scripts/web-merge.mjs";
3
5
  import { finalizeHardwareSelection } from "../../abelien-hardware/scripts/finalize-selection.mjs";
4
6
  import { canonical, digest, selectionRequestDigest, validateSelectionSlots } from "../../abelien-hardware/scripts/selection-request.mjs";
@@ -26,6 +28,7 @@ export async function selectHardware({ input, readResource, readBlob }) {
26
28
  || !Array.isArray(requirements.limits)
27
29
  || requirements.limits.some(item => !requirement(item) || !["hard", "preference"].includes(item.strength)))
28
30
  invalid("clarified requirements need attributed context, reaction and limits; limit strength must be hard or preference");
31
+ validateRequirementEnvelopes(requirements);
29
32
  const entries = [...requirements.reaction, ...requirements.limits];
30
33
  if (new Set(entries.map(item => item.id)).size !== entries.length)
31
34
  invalid("reaction and limit IDs must be unique across requirements");
@@ -53,6 +56,12 @@ export async function selectHardware({ input, readResource, readBlob }) {
53
56
  const candidate = recall.results.find(item => item.slot_id === slot.slot_id)?.candidates?.find(item => item.component_id === model.component_id);
54
57
  if (!candidate || canonical(candidate.category_path) !== canonical(slot.category_path)
55
58
  || !text(candidate.content) || !/^[a-f0-9]{64}$/.test(candidate.wiki?.sha256 ?? "")) invalid("candidate needs complete matching Wiki provenance");
59
+ if (createHash("sha256").update(candidate.content).digest("hex") !== candidate.wiki.sha256) invalid("candidate content does not match frozen Wiki SHA-256");
60
+ const dimensionCheck = evaluateDimensions(candidate, requirements);
61
+ model.dimension_check = dimensionCheck;
62
+ if (dimensionCheck.status === "incompatible") invalid("selected candidate is dimensionally incompatible");
63
+ // Only a user hard limit with trusted sizes asks the user to decide; unknown and other risk stay advisory on dimension_check.
64
+ if (dimensionCheck.decision_required) add("DIMENSION_DECISION_REQUIRED", { slot_id: slot.slot_id, component_id: candidate.component_id, dimension_check: dimensionCheck });
56
65
  if (!text(model.selection_reason)) invalid("selection_reason is required");
57
66
  const unmatched = model.wiki_evidence.filter(quote => !text(quote) || quote.trim().length < 8 || !candidate.content.includes(quote));
58
67
  if (unmatched.length) add("WIKI_EVIDENCE_UNVERIFIED", { slot_id: slot.slot_id,
@@ -6,7 +6,7 @@
6
6
  create 输入为 `schemaVersion:1, creationOperationId, inputRevision:0, manifestDigest, executionDefinitionVersion, executionDefinitionDigest, executionPath`;不接收任意来源正文或 provider/account/policy 覆盖。必须具有 `studio.create` 和 `tools:invoke` 委托。
7
7
  `creation active|get` 仅需 `state:read`,不借只读恢复索取工具或写入权限;创建、工具登记与调用提交仍须原有写权限,replacement request 仍须 `requests:write`。
8
8
  返回完整安全投影:保留 frozenInputs、plannedOccurrenceBindings、executionSources、进度与所有资产 Ref,不将派生外观来源丢弃。
9
- `tool register` 仅接受 creationOperationId/modificationOperationId 之一。CLI 默认受信上下文路径不变;模块的 contextFile 依赖注入只供受信宿主使用,不向模型开放路径、origin 或 token。
9
+ `tool register` 仅接受 creationOperationId/modificationOperationId 之一。CLI 默认受信上下文路径见下文「Studio broker 调用」;模块的 contextFile 依赖注入只供受信宿主使用,不向模型开放路径、origin 或 token。
10
10
  本地宿主必须保持原 Agent 租约,先将真实已结算 receipt 交 Harness consume/get 校验,再持久化消费确认;回执保存不等于采用、workflow done 或自动恢复。unknown 不重新生成。
11
11
 
12
12
  ## 定义文件(域包 `workflows/<name>.json`,name 必须等于文件名)
@@ -107,12 +107,19 @@ ab studio invocation get <invocationId>
107
107
  以同 workflow identity 和根预算创建后继 attempt。已有点选必须与原 designContext 一致;纯文字目标
108
108
  仍须由 Web 在当前 F2 editableTargets 唯一验证。CLI 不接受自由文字直接扩大 target 或 impact。
109
109
 
110
- CLI 只读固定 `/tmp/abelien-runtime-delegation/current.json`;不提供 context/origin/token override。
110
+ CLI 默认只读固定 `/home/agent/workspace/abelien-runtime-delegation/current.json`,与 Web EaaS driver
111
+ 的 file-API 挂载根一致;不搜索旧 `/tmp` 路径,也不提供 CLI/环境变量/模型 context、origin 或 token override。
112
+ 受信本地宿主仍可通过模块级 `studioCmd(...,{contextFile})` 注入隔离上下文;它不构成容器默认路径验收。
111
113
  宿主 context 必须是当前进程所有、普通文件、0600、至多 16 KiB,且 draft、logical environment、
112
114
  workflow、attempt、local run、generation、credential epoch、attempt fence、definition、action scope、
113
115
  签发/过期时间须与 token claims 全量一致。Web origin 只能是纯 HTTPS origin(测试 loopback 可 HTTP)。
114
116
  请求固定带 protocol v1、Bearer 且不带 Cookie;JSON 上限 256 KiB,响应上限 1 MiB。
115
117
 
118
+ G02 回归使用独立文件夹具执行默认 `readStudioContext()` 和未注入 `contextFile` 的 `studioCmd()`,
119
+ 同时检查旧路径不回退、宿主注入与安全边界。它只证明本地源码行为;SR-SPEC-09-01 仍须发布包与
120
+ 真实 driver 写入的容器上下文配合裸 CLI 验收,并核对安装器、默认 CLI、driver、probe、receipt launcher
121
+ 使用同一权威路径。源码修正不代表既有安装包已更新。
122
+
116
123
  create/register/submit 遇到传输未知只以相同字节尝试一次;再次未知返回 `STUDIO_WRITE_UNKNOWN`,
117
124
  不得换 modificationOperationId、commandId、operationId 或 invocationId。读取可有界退避。Agent 输出
118
125
  不包含 token,也不返回 provider/policy/account/price。普通首轮 production EaaS 尚无 attempt
@@ -208,3 +215,9 @@ advance、外部 resume 和 CLI status --httl 按记录摘要解析。依次尝
208
215
  同事件的状态机分支必须有可证明互斥的守卫(至少一个同名键的精确值不同)。可选 enabledBy 指定宿主环境变量,值必须为字符串 true;运行时分支歧义返回 DOMAIN_INVALID,守卫不满足仍为 GUARD_FAILED。verified 分支保持原守卫。
209
216
 
210
217
  两份新硬件定义声明 `legacyResume:read_only`:旧运行缺少新协议或摘要时,advance/resume/HTTL 返回带迁移说明的 WORKFLOW_DRIFTED,status/list 保留原记录。旧 pinned 定义也不能冒充新协议。新协议运行可按已记录 SHA 恢复,后续改开关不影响它;缺失或损坏摘要不能降级。其他无摘要 workflow 保留原步序列守卫。包内历史定义为审计和已支持的原协议恢复保留,不给旧选型补造摘要。package.json 和正式 CHANGELOG 版本由后续 release 维护,本 MR 不发布。
218
+
219
+ ### 局部外观与工程回执
220
+
221
+ 传输词表增加 `mesh.generate@1` 与 `cad.build@2`(purpose=appearance_adaptation),其余工具仍仅支持@1;实际步骤准入、完整 DTO 与既有 definition digest 由 Web broker 维护。creation v3 及旧 v1/v2 只按原 persisted identity 回读,不自动迁移。creation 状态允许 latestTool、cadBuildAttempts、previewCandidateId,与既有冻结来源并存。
222
+
223
+ receipt v1 增加 appearance.revision、mesh.generate、structure.adapt;这三步均必须携不可变 parentCandidateId,保持严格 creation/modification XOR,不能混入 repairBasis/repairOrdinal。EaaS waitIdentityVerified 另需核对 definitionDigest、stepKey 和 parentCandidateId;local checkpoint 仍交受信宿主判断,不代造已恢复或已采用。mesh 结果仅预览,structure.adapt 结果须同版工程和视觉检查后走既有采用事务。